diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index 5f524612ed..f76ebba8e9 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -49,7 +49,7 @@ jobs: with: python-version: "3.10" - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.10.0 + uses: docker/setup-buildx-action@v3.11.1 - name: Set TAG run: | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ca6d1b0aac..b7546012e9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -214,17 +214,51 @@ jobs: if: matrix.os == 'windows-latest' run: | ./venv/Scripts/activate - pytest -vv --cov-report=xml --tb=native -n auto tests + pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/ - name: Run pytest if: matrix.os == 'ubuntu-latest' || matrix.os == 'macOS-latest' run: | . venv/bin/activate - pytest -vv --cov-report=xml --tb=native -n auto tests + pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/ - name: Upload coverage to Codecov uses: codecov/codecov-action@v5.4.3 with: token: ${{ secrets.CODECOV_TOKEN }} + integration-tests: + name: Run integration tests + runs-on: ubuntu-latest + needs: + - common + steps: + - name: Check out code from GitHub + uses: actions/checkout@v4.2.2 + - name: Set up Python 3.13 + id: python + uses: actions/setup-python@v5.6.0 + with: + python-version: "3.13" + - name: Restore Python virtual environment + id: cache-venv + uses: actions/cache@v4.2.3 + with: + path: venv + key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }} + - name: Create Python virtual environment + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + python -m venv venv + . venv/bin/activate + python --version + pip install -r requirements.txt -r requirements_test.txt + pip install -e . + - name: Register matcher + run: echo "::add-matcher::.github/workflows/matchers/pytest.json" + - name: Run integration tests + run: | + . venv/bin/activate + pytest -vv --no-cov --tb=native -n auto tests/integration/ + clang-format: name: Check clang-format runs-on: ubuntu-24.04 @@ -494,6 +528,7 @@ jobs: - flake8 - pylint - pytest + - integration-tests - pyupgrade - clang-tidy - list-components diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index ee10f49f61..8806a89748 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -1,28 +1,11 @@ --- -name: Lock +name: Lock closed issues and PRs on: schedule: - - cron: "30 0 * * *" + - cron: "30 0 * * *" # Run daily at 00:30 UTC workflow_dispatch: -permissions: - issues: write - pull-requests: write - -concurrency: - group: lock - jobs: lock: - runs-on: ubuntu-latest - steps: - - uses: dessant/lock-threads@v5.0.1 - with: - pr-inactive-days: "1" - pr-lock-reason: "" - exclude-any-pr-labels: keep-open - - issue-inactive-days: "7" - issue-lock-reason: "" - exclude-any-issue-labels: keep-open + uses: esphome/workflows/.github/workflows/lock.yml@main diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index eae01fe0b3..b4518b27b5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -99,7 +99,7 @@ jobs: python-version: "3.10" - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.10.0 + uses: docker/setup-buildx-action@v3.11.1 - name: Log in to docker hub uses: docker/login-action@v3.4.0 @@ -178,7 +178,7 @@ jobs: merge-multiple: true - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.10.0 + uses: docker/setup-buildx-action@v3.11.1 - name: Log in to docker hub if: matrix.registry == 'dockerhub' diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d55c00eea7..831473c325 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.11.10 + rev: v0.12.2 hooks: # Run the linter. - id: ruff @@ -12,7 +12,7 @@ repos: # Run the formatter. - id: ruff-format - repo: https://github.com/PyCQA/flake8 - rev: 7.2.0 + rev: 7.3.0 hooks: - id: flake8 additional_dependencies: diff --git a/CODEOWNERS b/CODEOWNERS index a0812c9cd6..9b4681fcf2 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -87,6 +87,7 @@ esphome/components/bp1658cj/* @Cossid esphome/components/bp5758d/* @Cossid esphome/components/button/* @esphome/core esphome/components/bytebuffer/* @clydebarrow +esphome/components/camera/* @DT-art1 @bdraco esphome/components/canbus/* @danielschramm @mvturnho esphome/components/cap1188/* @mreditor97 esphome/components/captive_portal/* @OttoWinter @@ -124,6 +125,7 @@ esphome/components/dht/* @OttoWinter esphome/components/display_menu_base/* @numo68 esphome/components/dps310/* @kbx81 esphome/components/ds1307/* @badbadc0ffee +esphome/components/ds2484/* @mrk-its esphome/components/dsmr/* @glmnet @zuidwijk esphome/components/duty_time/* @dudanov esphome/components/ee895/* @Stock-M @@ -146,6 +148,7 @@ esphome/components/esp32_ble_client/* @jesserockz esphome/components/esp32_ble_server/* @Rapsssito @clydebarrow @jesserockz esphome/components/esp32_camera_web_server/* @ayufan esphome/components/esp32_can/* @Sympatron +esphome/components/esp32_hosted/* @swoboda1337 esphome/components/esp32_improv/* @jesserockz esphome/components/esp32_rmt/* @jesserockz esphome/components/esp32_rmt_led_strip/* @jesserockz @@ -167,6 +170,7 @@ esphome/components/ft5x06/* @clydebarrow esphome/components/ft63x6/* @gpambrozio esphome/components/gcja5/* @gcormier esphome/components/gdk101/* @Szewcson +esphome/components/gl_r01_i2c/* @pkejval esphome/components/globals/* @esphome/core esphome/components/gp2y1010au0f/* @zry98 esphome/components/gp8403/* @jesserockz @@ -247,9 +251,11 @@ esphome/components/libretiny_pwm/* @kuba2k2 esphome/components/light/* @esphome/core esphome/components/lightwaverf/* @max246 esphome/components/lilygo_t5_47/touchscreen/* @jesserockz +esphome/components/ln882x/* @lamauny esphome/components/lock/* @esphome/core esphome/components/logger/* @esphome/core esphome/components/logger/select/* @clydebarrow +esphome/components/lps22/* @nagisa esphome/components/ltr390/* @latonita @sjtrny esphome/components/ltr501/* @latonita esphome/components/ltr_als_ps/* @latonita @@ -323,6 +329,7 @@ esphome/components/one_wire/* @ssieb esphome/components/online_image/* @clydebarrow @guillempages esphome/components/opentherm/* @olegtarasov esphome/components/openthread/* @mrene +esphome/components/opt3001/* @ccutrer esphome/components/ota/* @esphome/core esphome/components/output/* @esphome/core esphome/components/packet_transport/* @clydebarrow @@ -330,6 +337,7 @@ esphome/components/pca6416a/* @Mat931 esphome/components/pca9554/* @clydebarrow @hwstar esphome/components/pcf85063/* @brogon esphome/components/pcf8563/* @KoenBreeman +esphome/components/pi4ioe5v6408/* @jesserockz esphome/components/pid/* @OttoWinter esphome/components/pipsolar/* @andreashergert1984 esphome/components/pm1006/* @habbie @@ -436,6 +444,8 @@ esphome/components/sun/* @OttoWinter esphome/components/sun_gtil2/* @Mat931 esphome/components/switch/* @esphome/core esphome/components/switch/binary_sensor/* @ssieb +esphome/components/sx126x/* @swoboda1337 +esphome/components/sx127x/* @swoboda1337 esphome/components/syslog/* @clydebarrow esphome/components/t6615/* @tylermenezes esphome/components/tc74/* @sethgirvan @@ -494,6 +504,7 @@ esphome/components/voice_assistant/* @jesserockz @kahrendt esphome/components/wake_on_lan/* @clydebarrow @willwill2will54 esphome/components/watchdog/* @oarcher esphome/components/waveshare_epaper/* @clydebarrow +esphome/components/web_server/ota/* @esphome/core esphome/components/web_server_base/* @OttoWinter esphome/components/web_server_idf/* @dentra esphome/components/weikai/* @DrCoolZic @@ -520,6 +531,7 @@ esphome/components/xiaomi_lywsd03mmc/* @ahpohl esphome/components/xiaomi_mhoc303/* @drug123 esphome/components/xiaomi_mhoc401/* @vevsvevs esphome/components/xiaomi_rtcgq02lm/* @jesserockz +esphome/components/xiaomi_xmwsdj04mmc/* @medusalix esphome/components/xl9535/* @mreditor97 esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68 esphome/components/xxtea/* @clydebarrow diff --git a/Doxyfile b/Doxyfile index 25212be96e..3d6147135b 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2025.6.3 +PROJECT_NUMBER = 2025.7.0b1 # 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 diff --git a/esphome/__main__.py b/esphome/__main__.py index 2dbdfeb1ff..d8a79c018a 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -34,11 +34,9 @@ from esphome.const import ( CONF_PORT, CONF_SUBSTITUTIONS, CONF_TOPIC, - PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040, - PLATFORM_RTL87XX, SECRETS_FILES, ) from esphome.core import CORE, EsphomeError, coroutine @@ -354,7 +352,7 @@ def upload_program(config, args, host): if CORE.target_platform in (PLATFORM_RP2040): return upload_using_platformio(config, args.device) - if CORE.target_platform in (PLATFORM_BK72XX, PLATFORM_RTL87XX): + if CORE.is_libretiny: return upload_using_platformio(config, host) return 1 # Unknown target platform diff --git a/esphome/codegen.py b/esphome/codegen.py index bfa1683ce7..8e02ec1164 100644 --- a/esphome/codegen.py +++ b/esphome/codegen.py @@ -22,6 +22,7 @@ from esphome.cpp_generator import ( # noqa: F401 TemplateArguments, add, add_build_flag, + add_build_unflag, add_define, add_global, add_library, @@ -34,6 +35,7 @@ from esphome.cpp_generator import ( # noqa: F401 process_lambda, progmem_array, safe_exp, + set_cpp_standard, statement, static_const_array, templatable, diff --git a/esphome/components/ac_dimmer/ac_dimmer.cpp b/esphome/components/ac_dimmer/ac_dimmer.cpp index ddaa910db3..e6f7a1214a 100644 --- a/esphome/components/ac_dimmer/ac_dimmer.cpp +++ b/esphome/components/ac_dimmer/ac_dimmer.cpp @@ -4,6 +4,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" #include +#include #ifdef USE_ESP8266 #include @@ -193,18 +194,17 @@ void AcDimmer::setup() { setTimer1Callback(&timer_interrupt); #endif #ifdef USE_ESP32 - // 80 Divider -> 1 count=1µs - dimmer_timer = timerBegin(0, 80, true); - timerAttachInterrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr, true); + // timer frequency of 1mhz + dimmer_timer = timerBegin(1000000); + timerAttachInterrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr); // For ESP32, we can't use dynamic interval calculation because the timerX functions // are not callable from ISR (placed in flash storage). // Here we just use an interrupt firing every 50 µs. - timerAlarmWrite(dimmer_timer, 50, true); - timerAlarmEnable(dimmer_timer); + timerAlarm(dimmer_timer, 50, true, 0); #endif } void AcDimmer::write_state(float state) { - state = std::acos(1 - (2 * state)) / 3.14159; // RMS power compensation + state = std::acos(1 - (2 * state)) / std::numbers::pi; // RMS power compensation auto new_value = static_cast(roundf(state * 65535)); if (new_value != 0 && this->store_.value == 0) this->store_.init_cycle = this->init_with_half_cycle_; diff --git a/esphome/components/adc/__init__.py b/esphome/components/adc/__init__.py index 5f94c61a08..10b7df8638 100644 --- a/esphome/components/adc/__init__.py +++ b/esphome/components/adc/__init__.py @@ -10,8 +10,15 @@ from esphome.components.esp32.const import ( VARIANT_ESP32S2, VARIANT_ESP32S3, ) +from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv -from esphome.const import CONF_ANALOG, CONF_INPUT, CONF_NUMBER, PLATFORM_ESP8266 +from esphome.const import ( + CONF_ANALOG, + CONF_INPUT, + CONF_NUMBER, + PLATFORM_ESP8266, + PlatformFramework, +) from esphome.core import CORE CODEOWNERS = ["@esphome/core"] @@ -229,3 +236,20 @@ def validate_adc_pin(value): )(value) raise NotImplementedError + + +FILTER_SOURCE_FILES = filter_source_files_from_platform( + { + "adc_sensor_esp32.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + }, + "adc_sensor_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, + "adc_sensor_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO}, + "adc_sensor_libretiny.cpp": { + PlatformFramework.BK72XX_ARDUINO, + PlatformFramework.RTL87XX_ARDUINO, + PlatformFramework.LN882X_ARDUINO, + }, + } +) diff --git a/esphome/components/adc/adc_sensor.h b/esphome/components/adc/adc_sensor.h index 62f2461245..28dfd2262c 100644 --- a/esphome/components/adc/adc_sensor.h +++ b/esphome/components/adc/adc_sensor.h @@ -15,8 +15,7 @@ namespace adc { #ifdef USE_ESP32 // clang-format off -#if (ESP_IDF_VERSION_MAJOR == 4 && ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 7)) || \ - (ESP_IDF_VERSION_MAJOR == 5 && \ +#if (ESP_IDF_VERSION_MAJOR == 5 && \ ((ESP_IDF_VERSION_MINOR == 0 && ESP_IDF_VERSION_PATCH >= 5) || \ (ESP_IDF_VERSION_MINOR == 1 && ESP_IDF_VERSION_PATCH >= 3) || \ (ESP_IDF_VERSION_MINOR >= 2)) \ @@ -28,19 +27,24 @@ static const adc_atten_t ADC_ATTEN_DB_12_COMPAT = ADC_ATTEN_DB_11; #endif #endif // USE_ESP32 -enum class SamplingMode : uint8_t { AVG = 0, MIN = 1, MAX = 2 }; +enum class SamplingMode : uint8_t { + AVG = 0, + MIN = 1, + MAX = 2, +}; + const LogString *sampling_mode_to_str(SamplingMode mode); class Aggregator { public: + Aggregator(SamplingMode mode); void add_sample(uint32_t value); uint32_t aggregate(); - Aggregator(SamplingMode mode); protected: - SamplingMode mode_{SamplingMode::AVG}; uint32_t aggr_{0}; uint32_t samples_{0}; + SamplingMode mode_{SamplingMode::AVG}; }; class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage_sampler::VoltageSampler { @@ -81,9 +85,9 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage #endif // USE_RP2040 protected: - InternalGPIOPin *pin_; - bool output_raw_{false}; uint8_t sample_count_{1}; + bool output_raw_{false}; + InternalGPIOPin *pin_; SamplingMode sampling_mode_{SamplingMode::AVG}; #ifdef USE_RP2040 @@ -95,11 +99,7 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage adc1_channel_t channel1_{ADC1_CHANNEL_MAX}; adc2_channel_t channel2_{ADC2_CHANNEL_MAX}; bool autorange_{false}; -#if ESP_IDF_VERSION_MAJOR >= 5 esp_adc_cal_characteristics_t cal_characteristics_[SOC_ADC_ATTEN_NUM] = {}; -#else - esp_adc_cal_characteristics_t cal_characteristics_[ADC_ATTEN_MAX] = {}; -#endif // ESP_IDF_VERSION_MAJOR #endif // USE_ESP32 }; diff --git a/esphome/components/adc/adc_sensor_common.cpp b/esphome/components/adc/adc_sensor_common.cpp index c7509c7c7a..797ab75045 100644 --- a/esphome/components/adc/adc_sensor_common.cpp +++ b/esphome/components/adc/adc_sensor_common.cpp @@ -61,7 +61,7 @@ uint32_t Aggregator::aggregate() { void ADCSensor::update() { float value_v = this->sample(); - ESP_LOGV(TAG, "'%s': Got voltage=%.4fV", this->get_name().c_str(), value_v); + ESP_LOGV(TAG, "'%s': Voltage=%.4fV", this->get_name().c_str(), value_v); this->publish_state(value_v); } diff --git a/esphome/components/adc/adc_sensor_esp32.cpp b/esphome/components/adc/adc_sensor_esp32.cpp index d6cf6e893b..ed1f3329ab 100644 --- a/esphome/components/adc/adc_sensor_esp32.cpp +++ b/esphome/components/adc/adc_sensor_esp32.cpp @@ -55,32 +55,40 @@ void ADCSensor::setup() { } void ADCSensor::dump_config() { + static const char *const ATTEN_AUTO_STR = "auto"; + static const char *const ATTEN_0DB_STR = "0 db"; + static const char *const ATTEN_2_5DB_STR = "2.5 db"; + static const char *const ATTEN_6DB_STR = "6 db"; + static const char *const ATTEN_12DB_STR = "12 db"; + const char *atten_str = ATTEN_AUTO_STR; + LOG_SENSOR("", "ADC Sensor", this); LOG_PIN(" Pin: ", this->pin_); - if (this->autorange_) { - ESP_LOGCONFIG(TAG, " Attenuation: auto"); - } else { + + if (!this->autorange_) { switch (this->attenuation_) { case ADC_ATTEN_DB_0: - ESP_LOGCONFIG(TAG, " Attenuation: 0db"); + atten_str = ATTEN_0DB_STR; break; case ADC_ATTEN_DB_2_5: - ESP_LOGCONFIG(TAG, " Attenuation: 2.5db"); + atten_str = ATTEN_2_5DB_STR; break; case ADC_ATTEN_DB_6: - ESP_LOGCONFIG(TAG, " Attenuation: 6db"); + atten_str = ATTEN_6DB_STR; break; case ADC_ATTEN_DB_12_COMPAT: - ESP_LOGCONFIG(TAG, " Attenuation: 12db"); + atten_str = ATTEN_12DB_STR; break; default: // This is to satisfy the unused ADC_ATTEN_MAX break; } } + ESP_LOGCONFIG(TAG, + " Attenuation: %s\n" " Samples: %i\n" " Sampling mode: %s", - this->sample_count_, LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_))); + atten_str, this->sample_count_, LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_))); LOG_UPDATE_INTERVAL(this); } diff --git a/esphome/components/ade7880/ade7880.h b/esphome/components/ade7880/ade7880.h index a565357dc5..40bc22e54a 100644 --- a/esphome/components/ade7880/ade7880.h +++ b/esphome/components/ade7880/ade7880.h @@ -85,8 +85,6 @@ class ADE7880 : public i2c::I2CDevice, public PollingComponent { void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } - protected: ADE7880Store store_{}; InternalGPIOPin *irq0_pin_{nullptr}; diff --git a/esphome/components/ads1115/ads1115.h b/esphome/components/ads1115/ads1115.h index e65835a386..e827a739d2 100644 --- a/esphome/components/ads1115/ads1115.h +++ b/esphome/components/ads1115/ads1115.h @@ -49,7 +49,6 @@ class ADS1115Component : public Component, public i2c::I2CDevice { void setup() override; void dump_config() override; /// HARDWARE_LATE setup priority - float get_setup_priority() const override { return setup_priority::DATA; } void set_continuous_mode(bool continuous_mode) { continuous_mode_ = continuous_mode; } /// Helper method to request a measurement from a sensor. diff --git a/esphome/components/ads1118/ads1118.h b/esphome/components/ads1118/ads1118.h index 8b9aa15cd2..e96baab386 100644 --- a/esphome/components/ads1118/ads1118.h +++ b/esphome/components/ads1118/ads1118.h @@ -34,7 +34,6 @@ class ADS1118 : public Component, ADS1118() = default; void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } /// Helper method to request a measurement from a sensor. float request_measurement(ADS1118Multiplexer multiplexer, ADS1118Gain gain, bool temperature_mode); diff --git a/esphome/components/ags10/ags10.h b/esphome/components/ags10/ags10.h index f2201fe70c..3e184ae176 100644 --- a/esphome/components/ags10/ags10.h +++ b/esphome/components/ags10/ags10.h @@ -31,8 +31,6 @@ class AGS10Component : public PollingComponent, public i2c::I2CDevice { void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } - /** * Modifies target address of AGS10. * diff --git a/esphome/components/aic3204/aic3204.h b/esphome/components/aic3204/aic3204.h index 783a58a2b9..28006e33fc 100644 --- a/esphome/components/aic3204/aic3204.h +++ b/esphome/components/aic3204/aic3204.h @@ -66,7 +66,6 @@ class AIC3204 : public audio_dac::AudioDac, public Component, public i2c::I2CDev public: void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } bool set_mute_off() override; bool set_mute_on() override; diff --git a/esphome/components/alarm_control_panel/__init__.py b/esphome/components/alarm_control_panel/__init__.py index e88050132a..6d37d53a4c 100644 --- a/esphome/components/alarm_control_panel/__init__.py +++ b/esphome/components/alarm_control_panel/__init__.py @@ -14,8 +14,8 @@ from esphome.const import ( CONF_WEB_SERVER, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@grahambrown11", "@hwstar"] IS_PLATFORM_COMPONENT = True @@ -149,6 +149,9 @@ _ALARM_CONTROL_PANEL_SCHEMA = ( ) +_ALARM_CONTROL_PANEL_SCHEMA.add_extra(entity_duplicate_validator("alarm_control_panel")) + + def alarm_control_panel_schema( class_: MockObjClass, *, @@ -190,7 +193,7 @@ ALARM_CONTROL_PANEL_CONDITION_SCHEMA = maybe_simple_id( async def setup_alarm_control_panel_core_(var, config): - await setup_entity(var, config) + await setup_entity(var, config, "alarm_control_panel") for conf in config.get(CONF_ON_STATE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) diff --git a/esphome/components/alpha3/alpha3.h b/esphome/components/alpha3/alpha3.h index 325c70a538..7189ecbc33 100644 --- a/esphome/components/alpha3/alpha3.h +++ b/esphome/components/alpha3/alpha3.h @@ -41,7 +41,6 @@ class Alpha3 : public esphome::ble_client::BLEClientNode, public PollingComponen void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_flow_sensor(sensor::Sensor *sensor) { this->flow_sensor_ = sensor; } void set_head_sensor(sensor::Sensor *sensor) { this->head_sensor_ = sensor; } void set_power_sensor(sensor::Sensor *sensor) { this->power_sensor_ = sensor; } diff --git a/esphome/components/am43/cover/am43_cover.h b/esphome/components/am43/cover/am43_cover.h index f33f2d1734..d6d020e98c 100644 --- a/esphome/components/am43/cover/am43_cover.h +++ b/esphome/components/am43/cover/am43_cover.h @@ -22,7 +22,6 @@ class Am43Component : public cover::Cover, public esphome::ble_client::BLEClient void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } cover::CoverTraits get_traits() override; void set_pin(uint16_t pin) { this->pin_ = pin; } void set_invert_position(bool invert_position) { this->invert_position_ = invert_position; } diff --git a/esphome/components/am43/sensor/am43_sensor.h b/esphome/components/am43/sensor/am43_sensor.h index 8dfe83e3a3..91973d8e33 100644 --- a/esphome/components/am43/sensor/am43_sensor.h +++ b/esphome/components/am43/sensor/am43_sensor.h @@ -22,7 +22,6 @@ class Am43 : public esphome::ble_client::BLEClientNode, public PollingComponent void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_battery(sensor::Sensor *battery) { battery_ = battery; } void set_illuminance(sensor::Sensor *illuminance) { illuminance_ = illuminance; } diff --git a/esphome/components/analog_threshold/analog_threshold_binary_sensor.h b/esphome/components/analog_threshold/analog_threshold_binary_sensor.h index efb8e3c90c..55d6b15c36 100644 --- a/esphome/components/analog_threshold/analog_threshold_binary_sensor.h +++ b/esphome/components/analog_threshold/analog_threshold_binary_sensor.h @@ -12,8 +12,6 @@ class AnalogThresholdBinarySensor : public Component, public binary_sensor::Bina void dump_config() override; void setup() override; - float get_setup_priority() const override { return setup_priority::DATA; } - void set_sensor(sensor::Sensor *analog_sensor); template void set_upper_threshold(T upper_threshold) { this->upper_threshold_ = upper_threshold; } template void set_lower_threshold(T lower_threshold) { this->lower_threshold_ = lower_threshold; } diff --git a/esphome/components/anova/anova.cpp b/esphome/components/anova/anova.cpp index ebf6c1d037..d0e8f6827f 100644 --- a/esphome/components/anova/anova.cpp +++ b/esphome/components/anova/anova.cpp @@ -17,7 +17,11 @@ void Anova::setup() { this->current_request_ = 0; } -void Anova::loop() {} +void Anova::loop() { + // Parent BLEClientNode has a loop() method, but this component uses + // polling via update() and BLE callbacks so loop isn't needed + this->disable_loop(); +} void Anova::control(const ClimateCall &call) { if (call.get_mode().has_value()) { diff --git a/esphome/components/anova/anova.h b/esphome/components/anova/anova.h index 3d1394980a..560d96baa7 100644 --- a/esphome/components/anova/anova.h +++ b/esphome/components/anova/anova.h @@ -26,7 +26,6 @@ class Anova : public climate::Climate, public esphome::ble_client::BLEClientNode void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } climate::ClimateTraits traits() override { auto traits = climate::ClimateTraits(); traits.set_supports_current_temperature(true); diff --git a/esphome/components/apds9960/apds9960.cpp b/esphome/components/apds9960/apds9960.cpp index 4a6ce371e5..b736e6b8b0 100644 --- a/esphome/components/apds9960/apds9960.cpp +++ b/esphome/components/apds9960/apds9960.cpp @@ -23,7 +23,7 @@ void APDS9960::setup() { 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->mark_failed(); return; diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index bd131ef8de..eb8883b025 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -3,6 +3,7 @@ import base64 from esphome import automation from esphome.automation import Condition import esphome.codegen as cg +from esphome.config_helpers import get_logger_level import esphome.config_validation as cv from esphome.const import ( CONF_ACTION, @@ -110,9 +111,10 @@ CONFIG_SCHEMA = cv.All( ): ACTIONS_SCHEMA, cv.Exclusive(CONF_ACTIONS, group_of_exclusion=CONF_ACTIONS): ACTIONS_SCHEMA, cv.Optional(CONF_ENCRYPTION): _encryption_schema, - cv.Optional( - CONF_BATCH_DELAY, default="100ms" - ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_BATCH_DELAY, default="100ms"): cv.All( + cv.positive_time_period_milliseconds, + cv.Range(max=cv.TimePeriod(milliseconds=65535)), + ), cv.Optional(CONF_ON_CLIENT_CONNECTED): automation.validate_automation( single=True ), @@ -131,27 +133,32 @@ async def to_code(config): await cg.register_component(var, config) cg.add(var.set_port(config[CONF_PORT])) - cg.add(var.set_password(config[CONF_PASSWORD])) + if config[CONF_PASSWORD]: + cg.add_define("USE_API_PASSWORD") + cg.add(var.set_password(config[CONF_PASSWORD])) cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) cg.add(var.set_batch_delay(config[CONF_BATCH_DELAY])) - for conf in config.get(CONF_ACTIONS, []): - template_args = [] - func_args = [] - service_arg_names = [] - for name, var_ in conf[CONF_VARIABLES].items(): - native = SERVICE_ARG_NATIVE_TYPES[var_] - template_args.append(native) - func_args.append((native, name)) - service_arg_names.append(name) - templ = cg.TemplateArguments(*template_args) - trigger = cg.new_Pvariable( - conf[CONF_TRIGGER_ID], templ, conf[CONF_ACTION], service_arg_names - ) - cg.add(var.register_user_service(trigger)) - await automation.build_automation(trigger, func_args, conf) + if actions := config.get(CONF_ACTIONS, []): + cg.add_define("USE_API_YAML_SERVICES") + for conf in actions: + template_args = [] + func_args = [] + service_arg_names = [] + for name, var_ in conf[CONF_VARIABLES].items(): + native = SERVICE_ARG_NATIVE_TYPES[var_] + template_args.append(native) + func_args.append((native, name)) + service_arg_names.append(name) + templ = cg.TemplateArguments(*template_args) + trigger = cg.new_Pvariable( + conf[CONF_TRIGGER_ID], templ, conf[CONF_ACTION], service_arg_names + ) + cg.add(var.register_user_service(trigger)) + await automation.build_automation(trigger, func_args, conf) if CONF_ON_CLIENT_CONNECTED in config: + cg.add_define("USE_API_CLIENT_CONNECTED_TRIGGER") await automation.build_automation( var.get_client_connected_trigger(), [(cg.std_string, "client_info"), (cg.std_string, "client_address")], @@ -159,6 +166,7 @@ async def to_code(config): ) if CONF_ON_CLIENT_DISCONNECTED in config: + cg.add_define("USE_API_CLIENT_DISCONNECTED_TRIGGER") await automation.build_automation( var.get_client_disconnected_trigger(), [(cg.std_string, "client_info"), (cg.std_string, "client_address")], @@ -177,7 +185,7 @@ async def to_code(config): # and plaintext disabled. Only a factory reset can remove it. cg.add_define("USE_API_PLAINTEXT") cg.add_define("USE_API_NOISE") - cg.add_library("esphome/noise-c", "0.1.6") + cg.add_library("esphome/noise-c", "0.1.10") else: cg.add_define("USE_API_PLAINTEXT") @@ -306,3 +314,17 @@ async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, arg @automation.register_condition("api.connected", APIConnectedCondition, {}) async def api_connected_to_code(config, condition_id, template_arg, args): return cg.new_Pvariable(condition_id, template_arg) + + +def FILTER_SOURCE_FILES() -> list[str]: + """Filter out api_pb2_dump.cpp when proto message dumping is not enabled.""" + # 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 + # all the way to the end even when ifdef'd out + # + # HAS_PROTO_MESSAGE_DUMP is defined when ESPHOME_LOG_HAS_VERY_VERBOSE is set, + # which happens when the logger level is VERY_VERBOSE + if get_logger_level() != "VERY_VERBOSE": + return ["api_pb2_dump.cpp"] + + return [] diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index b23652a982..c3795bb796 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -188,6 +188,17 @@ message DeviceInfoRequest { // Empty } +message AreaInfo { + uint32 area_id = 1; + string name = 2; +} + +message DeviceInfo { + uint32 device_id = 1; + string name = 2; + uint32 area_id = 3; +} + message DeviceInfoResponse { option (id) = 10; option (source) = SOURCE_SERVER; @@ -236,6 +247,12 @@ message DeviceInfoResponse { // Supports receiving and saving api encryption key bool api_encryption_supported = 19; + + repeated DeviceInfo devices = 20; + repeated AreaInfo areas = 21; + + // Top-level area info to phase out suggested_area + AreaInfo area = 22; } message ListEntitiesRequest { @@ -280,6 +297,7 @@ message ListEntitiesBinarySensorResponse { bool disabled_by_default = 7; string icon = 8; EntityCategory entity_category = 9; + uint32 device_id = 10; } message BinarySensorStateResponse { option (id) = 21; @@ -293,6 +311,7 @@ message BinarySensorStateResponse { // If the binary sensor does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller bool missing_state = 3; + uint32 device_id = 4; } // ==================== COVER ==================== @@ -315,6 +334,7 @@ message ListEntitiesCoverResponse { string icon = 10; EntityCategory entity_category = 11; bool supports_stop = 12; + uint32 device_id = 13; } enum LegacyCoverState { @@ -341,6 +361,7 @@ message CoverStateResponse { float position = 3; float tilt = 4; CoverOperation current_operation = 5; + uint32 device_id = 6; } enum LegacyCoverCommand { @@ -388,6 +409,7 @@ message ListEntitiesFanResponse { string icon = 10; EntityCategory entity_category = 11; repeated string supported_preset_modes = 12; + uint32 device_id = 13; } enum FanSpeed { FAN_SPEED_LOW = 0; @@ -412,6 +434,7 @@ message FanStateResponse { FanDirection direction = 5; int32 speed_level = 6; string preset_mode = 7; + uint32 device_id = 8; } message FanCommandRequest { option (id) = 31; @@ -471,6 +494,7 @@ message ListEntitiesLightResponse { bool disabled_by_default = 13; string icon = 14; EntityCategory entity_category = 15; + uint32 device_id = 16; } message LightStateResponse { option (id) = 24; @@ -492,6 +516,7 @@ message LightStateResponse { float cold_white = 12; float warm_white = 13; string effect = 9; + uint32 device_id = 14; } message LightCommandRequest { option (id) = 32; @@ -563,6 +588,7 @@ message ListEntitiesSensorResponse { SensorLastResetType legacy_last_reset_type = 11; bool disabled_by_default = 12; EntityCategory entity_category = 13; + uint32 device_id = 14; } message SensorStateResponse { option (id) = 25; @@ -576,6 +602,7 @@ message SensorStateResponse { // If the sensor does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller bool missing_state = 3; + uint32 device_id = 4; } // ==================== SWITCH ==================== @@ -595,6 +622,7 @@ message ListEntitiesSwitchResponse { bool disabled_by_default = 7; EntityCategory entity_category = 8; string device_class = 9; + uint32 device_id = 10; } message SwitchStateResponse { option (id) = 26; @@ -605,6 +633,7 @@ message SwitchStateResponse { fixed32 key = 1; bool state = 2; + uint32 device_id = 3; } message SwitchCommandRequest { option (id) = 33; @@ -632,6 +661,7 @@ message ListEntitiesTextSensorResponse { bool disabled_by_default = 6; EntityCategory entity_category = 7; string device_class = 8; + uint32 device_id = 9; } message TextSensorStateResponse { option (id) = 27; @@ -645,6 +675,7 @@ message TextSensorStateResponse { // If the text sensor does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller bool missing_state = 3; + uint32 device_id = 4; } // ==================== SUBSCRIBE LOGS ==================== @@ -805,7 +836,7 @@ message ListEntitiesCameraResponse { option (id) = 43; option (base_class) = "InfoResponseProtoMessage"; option (source) = SOURCE_SERVER; - option (ifdef) = "USE_ESP32_CAMERA"; + option (ifdef) = "USE_CAMERA"; string object_id = 1; fixed32 key = 2; @@ -814,12 +845,13 @@ message ListEntitiesCameraResponse { bool disabled_by_default = 5; string icon = 6; EntityCategory entity_category = 7; + uint32 device_id = 8; } message CameraImageResponse { option (id) = 44; option (source) = SOURCE_SERVER; - option (ifdef) = "USE_ESP32_CAMERA"; + option (ifdef) = "USE_CAMERA"; fixed32 key = 1; bytes data = 2; @@ -828,7 +860,7 @@ message CameraImageResponse { message CameraImageRequest { option (id) = 45; option (source) = SOURCE_CLIENT; - option (ifdef) = "USE_ESP32_CAMERA"; + option (ifdef) = "USE_CAMERA"; option (no_delay) = true; bool single = 1; @@ -916,6 +948,7 @@ message ListEntitiesClimateResponse { bool supports_target_humidity = 23; float visual_min_humidity = 24; float visual_max_humidity = 25; + uint32 device_id = 26; } message ClimateStateResponse { option (id) = 47; @@ -940,6 +973,7 @@ message ClimateStateResponse { string custom_preset = 13; float current_humidity = 14; float target_humidity = 15; + uint32 device_id = 16; } message ClimateCommandRequest { option (id) = 48; @@ -999,6 +1033,7 @@ message ListEntitiesNumberResponse { string unit_of_measurement = 11; NumberMode mode = 12; string device_class = 13; + uint32 device_id = 14; } message NumberStateResponse { option (id) = 50; @@ -1012,6 +1047,7 @@ message NumberStateResponse { // If the number does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller bool missing_state = 3; + uint32 device_id = 4; } message NumberCommandRequest { option (id) = 51; @@ -1039,6 +1075,7 @@ message ListEntitiesSelectResponse { repeated string options = 6; bool disabled_by_default = 7; EntityCategory entity_category = 8; + uint32 device_id = 9; } message SelectStateResponse { option (id) = 53; @@ -1052,6 +1089,7 @@ message SelectStateResponse { // If the select does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller bool missing_state = 3; + uint32 device_id = 4; } message SelectCommandRequest { option (id) = 54; @@ -1081,6 +1119,7 @@ message ListEntitiesSirenResponse { bool supports_duration = 8; bool supports_volume = 9; EntityCategory entity_category = 10; + uint32 device_id = 11; } message SirenStateResponse { option (id) = 56; @@ -1091,6 +1130,7 @@ message SirenStateResponse { fixed32 key = 1; bool state = 2; + uint32 device_id = 3; } message SirenCommandRequest { option (id) = 57; @@ -1144,6 +1184,7 @@ message ListEntitiesLockResponse { // Not yet implemented: string code_format = 11; + uint32 device_id = 12; } message LockStateResponse { option (id) = 59; @@ -1153,6 +1194,7 @@ message LockStateResponse { option (no_delay) = true; fixed32 key = 1; LockState state = 2; + uint32 device_id = 3; } message LockCommandRequest { option (id) = 60; @@ -1183,6 +1225,7 @@ message ListEntitiesButtonResponse { bool disabled_by_default = 6; EntityCategory entity_category = 7; string device_class = 8; + uint32 device_id = 9; } message ButtonCommandRequest { option (id) = 62; @@ -1238,6 +1281,8 @@ message ListEntitiesMediaPlayerResponse { bool supports_pause = 8; repeated MediaPlayerSupportedFormat supported_formats = 9; + + uint32 device_id = 10; } message MediaPlayerStateResponse { option (id) = 64; @@ -1249,6 +1294,7 @@ message MediaPlayerStateResponse { MediaPlayerState state = 2; float volume = 3; bool muted = 4; + uint32 device_id = 5; } message MediaPlayerCommandRequest { option (id) = 65; @@ -1778,6 +1824,7 @@ message ListEntitiesAlarmControlPanelResponse { uint32 supported_features = 8; bool requires_code = 9; bool requires_code_to_arm = 10; + uint32 device_id = 11; } message AlarmControlPanelStateResponse { @@ -1788,6 +1835,7 @@ message AlarmControlPanelStateResponse { option (no_delay) = true; fixed32 key = 1; AlarmControlPanelState state = 2; + uint32 device_id = 3; } message AlarmControlPanelCommandRequest { @@ -1823,6 +1871,7 @@ message ListEntitiesTextResponse { uint32 max_length = 9; string pattern = 10; TextMode mode = 11; + uint32 device_id = 12; } message TextStateResponse { option (id) = 98; @@ -1836,6 +1885,7 @@ message TextStateResponse { // If the Text does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller bool missing_state = 3; + uint32 device_id = 4; } message TextCommandRequest { option (id) = 99; @@ -1863,6 +1913,7 @@ message ListEntitiesDateResponse { string icon = 5; bool disabled_by_default = 6; EntityCategory entity_category = 7; + uint32 device_id = 8; } message DateStateResponse { option (id) = 101; @@ -1878,6 +1929,7 @@ message DateStateResponse { uint32 year = 3; uint32 month = 4; uint32 day = 5; + uint32 device_id = 6; } message DateCommandRequest { option (id) = 102; @@ -1906,6 +1958,7 @@ message ListEntitiesTimeResponse { string icon = 5; bool disabled_by_default = 6; EntityCategory entity_category = 7; + uint32 device_id = 8; } message TimeStateResponse { option (id) = 104; @@ -1921,6 +1974,7 @@ message TimeStateResponse { uint32 hour = 3; uint32 minute = 4; uint32 second = 5; + uint32 device_id = 6; } message TimeCommandRequest { option (id) = 105; @@ -1952,6 +2006,7 @@ message ListEntitiesEventResponse { string device_class = 8; repeated string event_types = 9; + uint32 device_id = 10; } message EventResponse { option (id) = 108; @@ -1961,6 +2016,7 @@ message EventResponse { fixed32 key = 1; string event_type = 2; + uint32 device_id = 3; } // ==================== VALVE ==================== @@ -1983,6 +2039,7 @@ message ListEntitiesValveResponse { bool assumed_state = 9; bool supports_position = 10; bool supports_stop = 11; + uint32 device_id = 12; } enum ValveOperation { @@ -2000,6 +2057,7 @@ message ValveStateResponse { fixed32 key = 1; float position = 2; ValveOperation current_operation = 3; + uint32 device_id = 4; } message ValveCommandRequest { @@ -2029,6 +2087,7 @@ message ListEntitiesDateTimeResponse { string icon = 5; bool disabled_by_default = 6; EntityCategory entity_category = 7; + uint32 device_id = 8; } message DateTimeStateResponse { option (id) = 113; @@ -2042,6 +2101,7 @@ message DateTimeStateResponse { // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller bool missing_state = 2; fixed32 epoch_seconds = 3; + uint32 device_id = 4; } message DateTimeCommandRequest { option (id) = 114; @@ -2069,6 +2129,7 @@ message ListEntitiesUpdateResponse { bool disabled_by_default = 6; EntityCategory entity_category = 7; string device_class = 8; + uint32 device_id = 9; } message UpdateStateResponse { option (id) = 117; @@ -2087,6 +2148,7 @@ message UpdateStateResponse { string title = 8; string release_summary = 9; string release_url = 10; + uint32 device_id = 11; } enum UpdateCommand { UPDATE_COMMAND_NONE = 0; diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 3e2b7c0154..779784e787 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -28,8 +28,32 @@ namespace esphome { namespace api { +// Read a maximum of 5 messages per loop iteration to prevent starving other components. +// This is a balance between API responsiveness and allowing other components to run. +// Since each message could contain multiple protobuf messages when using packet batching, +// this limits the number of messages processed, not the number of TCP packets. +static constexpr uint8_t MAX_MESSAGES_PER_LOOP = 5; +static constexpr uint8_t MAX_PING_RETRIES = 60; +static constexpr uint16_t PING_RETRY_INTERVAL = 1000; +static constexpr uint32_t KEEPALIVE_DISCONNECT_TIMEOUT = (KEEPALIVE_TIMEOUT_MS * 5) / 2; + static const char *const TAG = "api.connection"; -static const int ESP32_CAMERA_STOP_STREAM = 5000; +#ifdef USE_CAMERA +static const int CAMERA_STOP_STREAM = 5000; +#endif + +// Helper macro for entity command handlers - gets entity by key, returns if not found, and creates call object +#define ENTITY_COMMAND_MAKE_CALL(entity_type, entity_var, getter_name) \ + entity_type *entity_var = App.get_##getter_name##_by_key(msg.key); \ + if ((entity_var) == nullptr) \ + return; \ + auto call = (entity_var)->make_call(); + +// Helper macro for entity command handlers that don't use make_call() - gets entity by key and returns if not found +#define ENTITY_COMMAND_GET(entity_type, entity_var, getter_name) \ + entity_type *entity_var = App.get_##getter_name##_by_key(msg.key); \ + if ((entity_var) == nullptr) \ + return; APIConnection::APIConnection(std::unique_ptr sock, APIServer *parent) : parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) { @@ -47,6 +71,11 @@ APIConnection::APIConnection(std::unique_ptr sock, APIServer *pa #else #error "No frame helper defined" #endif +#ifdef USE_CAMERA + if (camera::Camera::instance() != nullptr) { + this->image_reader_ = std::unique_ptr{camera::Camera::instance()->create_image_reader()}; + } +#endif } uint32_t APIConnection::get_batch_delay_ms_() const { return this->parent_->get_batch_delay(); } @@ -54,15 +83,11 @@ uint32_t APIConnection::get_batch_delay_ms_() const { return this->parent_->get_ void APIConnection::start() { this->last_traffic_ = App.get_loop_component_start_time(); - // Set next_ping_retry_ to prevent immediate ping - // This ensures the first ping happens after the keepalive period - this->next_ping_retry_ = this->last_traffic_ + KEEPALIVE_TIMEOUT_MS; - APIError err = this->helper_->init(); if (err != APIError::OK) { on_fatal_error(); - ESP_LOGW(TAG, "%s: Helper init failed: %s errno=%d", this->client_combined_info_.c_str(), api_error_to_str(err), - errno); + ESP_LOGW(TAG, "%s: Helper init failed: %s errno=%d", this->get_client_combined_info().c_str(), + api_error_to_str(err), errno); return; } this->client_info_ = helper_->getpeername(); @@ -84,104 +109,99 @@ APIConnection::~APIConnection() { } void APIConnection::loop() { - if (this->remove_) - return; - - if (!network::is_connected()) { - // when network is disconnected force disconnect immediately - // don't wait for timeout - this->on_fatal_error(); - ESP_LOGW(TAG, "%s: Network unavailable; disconnecting", this->client_combined_info_.c_str()); - return; - } - if (this->next_close_) { + if (this->flags_.next_close) { // requested a disconnect this->helper_->close(); - this->remove_ = true; + this->flags_.remove = true; return; } APIError err = this->helper_->loop(); if (err != APIError::OK) { on_fatal_error(); - ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->client_combined_info_.c_str(), + ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->get_client_combined_info().c_str(), api_error_to_str(err), errno); return; } + const uint32_t now = App.get_loop_component_start_time(); // Check if socket has data ready before attempting to read if (this->helper_->is_socket_ready()) { - ReadPacketBuffer buffer; - err = this->helper_->read_packet(&buffer); - if (err == APIError::WOULD_BLOCK) { - // pass - } else if (err != APIError::OK) { - on_fatal_error(); - if (err == APIError::SOCKET_READ_FAILED && errno == ECONNRESET) { - ESP_LOGW(TAG, "%s: Connection reset", this->client_combined_info_.c_str()); - } else if (err == APIError::CONNECTION_CLOSED) { - ESP_LOGW(TAG, "%s: Connection closed", this->client_combined_info_.c_str()); - } else { - ESP_LOGW(TAG, "%s: Reading failed: %s errno=%d", this->client_combined_info_.c_str(), api_error_to_str(err), - errno); - } - return; - } else { - this->last_traffic_ = App.get_loop_component_start_time(); - // read a packet - if (buffer.data_len > 0) { - this->read_message(buffer.data_len, buffer.type, &buffer.container[buffer.data_offset]); - } else { - this->read_message(0, buffer.type, nullptr); - } - if (this->remove_) + // Read up to MAX_MESSAGES_PER_LOOP messages per loop to improve throughput + for (uint8_t message_count = 0; message_count < MAX_MESSAGES_PER_LOOP; message_count++) { + ReadPacketBuffer buffer; + err = this->helper_->read_packet(&buffer); + if (err == APIError::WOULD_BLOCK) { + // No more data available + break; + } else if (err != APIError::OK) { + on_fatal_error(); + if (err == APIError::SOCKET_READ_FAILED && errno == ECONNRESET) { + ESP_LOGW(TAG, "%s: Connection reset", this->get_client_combined_info().c_str()); + } else if (err == APIError::CONNECTION_CLOSED) { + ESP_LOGW(TAG, "%s: Connection closed", this->get_client_combined_info().c_str()); + } else { + ESP_LOGW(TAG, "%s: Reading failed: %s errno=%d", this->get_client_combined_info().c_str(), + api_error_to_str(err), errno); + } return; + } else { + this->last_traffic_ = now; + // read a packet + if (buffer.data_len > 0) { + this->read_message(buffer.data_len, buffer.type, &buffer.container[buffer.data_offset]); + } else { + this->read_message(0, buffer.type, nullptr); + } + if (this->flags_.remove) + return; + } } } - // Process deferred batch if scheduled - if (this->deferred_batch_.batch_scheduled && - App.get_loop_component_start_time() - this->deferred_batch_.batch_start_time >= this->get_batch_delay_ms_()) { + // Process deferred batch if scheduled and timer has expired + if (this->flags_.batch_scheduled && now - this->deferred_batch_.batch_start_time >= this->get_batch_delay_ms_()) { this->process_batch_(); } - if (!this->list_entities_iterator_.completed()) - this->list_entities_iterator_.advance(); - if (!this->initial_state_iterator_.completed() && this->list_entities_iterator_.completed()) - this->initial_state_iterator_.advance(); + if (!this->list_entities_iterator_.completed()) { + this->process_iterator_batch_(this->list_entities_iterator_); + } else if (!this->initial_state_iterator_.completed()) { + this->process_iterator_batch_(this->initial_state_iterator_); - static uint8_t max_ping_retries = 60; - static uint16_t ping_retry_interval = 1000; - const uint32_t now = App.get_loop_component_start_time(); - if (this->sent_ping_) { - // Disconnect if not responded within 2.5*keepalive - if (now - this->last_traffic_ > (KEEPALIVE_TIMEOUT_MS * 5) / 2) { - on_fatal_error(); - ESP_LOGW(TAG, "%s is unresponsive; disconnecting", this->client_combined_info_.c_str()); - } - } else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS && now > this->next_ping_retry_) { - ESP_LOGVV(TAG, "Sending keepalive PING"); - this->sent_ping_ = this->send_message(PingRequest()); - if (!this->sent_ping_) { - this->next_ping_retry_ = now + ping_retry_interval; - this->ping_retries_++; - std::string warn_str = str_sprintf("%s: Sending keepalive failed %u time(s);", - this->client_combined_info_.c_str(), this->ping_retries_); - if (this->ping_retries_ >= max_ping_retries) { - on_fatal_error(); - ESP_LOGE(TAG, "%s disconnecting", warn_str.c_str()); - } else if (this->ping_retries_ >= 10) { - ESP_LOGW(TAG, "%s retrying in %u ms", warn_str.c_str(), ping_retry_interval); - } else { - ESP_LOGD(TAG, "%s retrying in %u ms", warn_str.c_str(), ping_retry_interval); + // If we've completed initial states, process any remaining and clear the flag + if (this->initial_state_iterator_.completed()) { + // Process any remaining batched messages immediately + if (!this->deferred_batch_.empty()) { + this->process_batch_(); } + // Now that everything is sent, enable immediate sending for future state changes + this->flags_.should_try_send_immediately = true; } } -#ifdef USE_ESP32_CAMERA - if (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()); - bool done = this->image_reader_.available() == to_send; + if (this->flags_.sent_ping) { + // Disconnect if not responded within 2.5*keepalive + if (now - this->last_traffic_ > KEEPALIVE_DISCONNECT_TIMEOUT) { + on_fatal_error(); + ESP_LOGW(TAG, "%s is unresponsive; disconnecting", this->get_client_combined_info().c_str()); + } + } else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS) { + ESP_LOGVV(TAG, "Sending keepalive PING"); + this->flags_.sent_ping = this->send_message(PingRequest()); + if (!this->flags_.sent_ping) { + // If we can't send the ping request directly (tx_buffer full), + // schedule it at the front of the batch so it will be sent with priority + ESP_LOGW(TAG, "Buffer full, ping queued"); + this->schedule_message_front_(nullptr, &APIConnection::try_send_ping_request, PingRequest::MESSAGE_TYPE); + this->flags_.sent_ping = true; // Mark as sent to avoid scheduling multiple pings + } + } + +#ifdef USE_CAMERA + 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()); + bool done = this->image_reader_->available() == to_send; uint32_t msg_size = 0; ProtoSize::add_fixed_field<4>(msg_size, 1, true); // partial message size calculated manually since its a special case @@ -191,28 +211,26 @@ void APIConnection::loop() { auto buffer = this->create_buffer(msg_size); // fixed32 key = 1; - buffer.encode_fixed32(1, esp32_camera::global_esp32_camera->get_object_id_hash()); + buffer.encode_fixed32(1, camera::Camera::instance()->get_object_id_hash()); // bytes data = 2; - buffer.encode_bytes(2, this->image_reader_.peek_data_buffer(), to_send); + buffer.encode_bytes(2, this->image_reader_->peek_data_buffer(), to_send); // bool done = 3; buffer.encode_bool(3, done); - bool success = this->send_buffer(buffer, 44); + bool success = this->send_buffer(buffer, CameraImageResponse::MESSAGE_TYPE); if (success) { - this->image_reader_.consume_data(to_send); - } - if (success && done) { - this->image_reader_.return_image(); + this->image_reader_->consume_data(to_send); + if (done) { + this->image_reader_->return_image(); + } } } #endif - if (state_subs_at_ != -1) { + if (state_subs_at_ >= 0) { const auto &subs = this->parent_->get_state_subs(); - if (state_subs_at_ >= (int) subs.size()) { - state_subs_at_ = -1; - } else { + if (state_subs_at_ < static_cast(subs.size())) { auto &it = subs[state_subs_at_]; SubscribeHomeAssistantStateResponse resp; resp.entity_id = it.entity_id; @@ -221,6 +239,8 @@ void APIConnection::loop() { if (this->send_message(resp)) { state_subs_at_++; } + } else { + state_subs_at_ = -1; } } } @@ -233,20 +253,28 @@ DisconnectResponse APIConnection::disconnect(const DisconnectRequest &msg) { // remote initiated disconnect_client // don't close yet, we still need to send the disconnect response // close will happen on next loop - ESP_LOGD(TAG, "%s disconnected", this->client_combined_info_.c_str()); - this->next_close_ = true; + ESP_LOGD(TAG, "%s disconnected", this->get_client_combined_info().c_str()); + this->flags_.next_close = true; DisconnectResponse resp; return resp; } void APIConnection::on_disconnect_response(const DisconnectResponse &value) { this->helper_->close(); - this->remove_ = true; + this->flags_.remove = true; } // Encodes a message to the buffer and returns the total number of bytes used, // including header and footer overhead. Returns 0 if the message doesn't fit. uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint16_t message_type, APIConnection *conn, uint32_t remaining_size, bool is_single) { +#ifdef HAS_PROTO_MESSAGE_DUMP + // If in log-only mode, just log and return + if (conn->flags_.log_only_mode) { + conn->log_send_message_(msg.message_name(), msg.dump()); + return 1; // Return non-zero to indicate "success" for logging + } +#endif + // Calculate size uint32_t calculated_size = 0; msg.calculate_size(calculated_size); @@ -287,12 +315,8 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint16_t mes #ifdef USE_BINARY_SENSOR bool APIConnection::send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor) { - return this->schedule_message_(binary_sensor, &APIConnection::try_send_binary_sensor_state, - BinarySensorStateResponse::MESSAGE_TYPE); -} -void APIConnection::send_binary_sensor_info(binary_sensor::BinarySensor *binary_sensor) { - this->schedule_message_(binary_sensor, &APIConnection::try_send_binary_sensor_info, - ListEntitiesBinarySensorResponse::MESSAGE_TYPE); + return this->send_message_smart_(binary_sensor, &APIConnection::try_send_binary_sensor_state, + BinarySensorStateResponse::MESSAGE_TYPE); } uint16_t APIConnection::try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, @@ -319,10 +343,7 @@ uint16_t APIConnection::try_send_binary_sensor_info(EntityBase *entity, APIConne #ifdef USE_COVER bool APIConnection::send_cover_state(cover::Cover *cover) { - return this->schedule_message_(cover, &APIConnection::try_send_cover_state, CoverStateResponse::MESSAGE_TYPE); -} -void APIConnection::send_cover_info(cover::Cover *cover) { - this->schedule_message_(cover, &APIConnection::try_send_cover_info, ListEntitiesCoverResponse::MESSAGE_TYPE); + return this->send_message_smart_(cover, &APIConnection::try_send_cover_state, CoverStateResponse::MESSAGE_TYPE); } uint16_t APIConnection::try_send_cover_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -353,11 +374,7 @@ uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *c return encode_message_to_buffer(msg, ListEntitiesCoverResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } void APIConnection::cover_command(const CoverCommandRequest &msg) { - cover::Cover *cover = App.get_cover_by_key(msg.key); - if (cover == nullptr) - return; - - auto call = cover->make_call(); + ENTITY_COMMAND_MAKE_CALL(cover::Cover, cover, cover) if (msg.has_legacy_command) { switch (msg.legacy_command) { case enums::LEGACY_COVER_COMMAND_OPEN: @@ -383,10 +400,7 @@ void APIConnection::cover_command(const CoverCommandRequest &msg) { #ifdef USE_FAN bool APIConnection::send_fan_state(fan::Fan *fan) { - return this->schedule_message_(fan, &APIConnection::try_send_fan_state, FanStateResponse::MESSAGE_TYPE); -} -void APIConnection::send_fan_info(fan::Fan *fan) { - this->schedule_message_(fan, &APIConnection::try_send_fan_info, ListEntitiesFanResponse::MESSAGE_TYPE); + return this->send_message_smart_(fan, &APIConnection::try_send_fan_state, FanStateResponse::MESSAGE_TYPE); } uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -422,11 +436,7 @@ uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *con return encode_message_to_buffer(msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } void APIConnection::fan_command(const FanCommandRequest &msg) { - fan::Fan *fan = App.get_fan_by_key(msg.key); - if (fan == nullptr) - return; - - auto call = fan->make_call(); + ENTITY_COMMAND_MAKE_CALL(fan::Fan, fan, fan) if (msg.has_state) call.set_state(msg.state); if (msg.has_oscillating) @@ -445,10 +455,7 @@ void APIConnection::fan_command(const FanCommandRequest &msg) { #ifdef USE_LIGHT bool APIConnection::send_light_state(light::LightState *light) { - return this->schedule_message_(light, &APIConnection::try_send_light_state, LightStateResponse::MESSAGE_TYPE); -} -void APIConnection::send_light_info(light::LightState *light) { - this->schedule_message_(light, &APIConnection::try_send_light_info, ListEntitiesLightResponse::MESSAGE_TYPE); + return this->send_message_smart_(light, &APIConnection::try_send_light_state, LightStateResponse::MESSAGE_TYPE); } uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -502,11 +509,7 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c return encode_message_to_buffer(msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } void APIConnection::light_command(const LightCommandRequest &msg) { - light::LightState *light = App.get_light_by_key(msg.key); - if (light == nullptr) - return; - - auto call = light->make_call(); + ENTITY_COMMAND_MAKE_CALL(light::LightState, light, light) if (msg.has_state) call.set_state(msg.state); if (msg.has_brightness) @@ -540,10 +543,7 @@ void APIConnection::light_command(const LightCommandRequest &msg) { #ifdef USE_SENSOR bool APIConnection::send_sensor_state(sensor::Sensor *sensor) { - return this->schedule_message_(sensor, &APIConnection::try_send_sensor_state, SensorStateResponse::MESSAGE_TYPE); -} -void APIConnection::send_sensor_info(sensor::Sensor *sensor) { - this->schedule_message_(sensor, &APIConnection::try_send_sensor_info, ListEntitiesSensorResponse::MESSAGE_TYPE); + return this->send_message_smart_(sensor, &APIConnection::try_send_sensor_state, SensorStateResponse::MESSAGE_TYPE); } uint16_t APIConnection::try_send_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, @@ -575,10 +575,7 @@ uint16_t APIConnection::try_send_sensor_info(EntityBase *entity, APIConnection * #ifdef USE_SWITCH bool APIConnection::send_switch_state(switch_::Switch *a_switch) { - return this->schedule_message_(a_switch, &APIConnection::try_send_switch_state, SwitchStateResponse::MESSAGE_TYPE); -} -void APIConnection::send_switch_info(switch_::Switch *a_switch) { - this->schedule_message_(a_switch, &APIConnection::try_send_switch_info, ListEntitiesSwitchResponse::MESSAGE_TYPE); + return this->send_message_smart_(a_switch, &APIConnection::try_send_switch_state, SwitchStateResponse::MESSAGE_TYPE); } uint16_t APIConnection::try_send_switch_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, @@ -601,9 +598,7 @@ uint16_t APIConnection::try_send_switch_info(EntityBase *entity, APIConnection * return encode_message_to_buffer(msg, ListEntitiesSwitchResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } void APIConnection::switch_command(const SwitchCommandRequest &msg) { - switch_::Switch *a_switch = App.get_switch_by_key(msg.key); - if (a_switch == nullptr) - return; + ENTITY_COMMAND_GET(switch_::Switch, a_switch, switch) if (msg.state) { a_switch->turn_on(); @@ -615,12 +610,8 @@ void APIConnection::switch_command(const SwitchCommandRequest &msg) { #ifdef USE_TEXT_SENSOR bool APIConnection::send_text_sensor_state(text_sensor::TextSensor *text_sensor) { - return this->schedule_message_(text_sensor, &APIConnection::try_send_text_sensor_state, - TextSensorStateResponse::MESSAGE_TYPE); -} -void APIConnection::send_text_sensor_info(text_sensor::TextSensor *text_sensor) { - this->schedule_message_(text_sensor, &APIConnection::try_send_text_sensor_info, - ListEntitiesTextSensorResponse::MESSAGE_TYPE); + return this->send_message_smart_(text_sensor, &APIConnection::try_send_text_sensor_state, + TextSensorStateResponse::MESSAGE_TYPE); } uint16_t APIConnection::try_send_text_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, @@ -647,7 +638,7 @@ uint16_t APIConnection::try_send_text_sensor_info(EntityBase *entity, APIConnect #ifdef USE_CLIMATE bool APIConnection::send_climate_state(climate::Climate *climate) { - return this->schedule_message_(climate, &APIConnection::try_send_climate_state, ClimateStateResponse::MESSAGE_TYPE); + return this->send_message_smart_(climate, &APIConnection::try_send_climate_state, ClimateStateResponse::MESSAGE_TYPE); } uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -682,9 +673,6 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection resp.target_humidity = climate->target_humidity; return encode_message_to_buffer(resp, ClimateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } -void APIConnection::send_climate_info(climate::Climate *climate) { - this->schedule_message_(climate, &APIConnection::try_send_climate_info, ListEntitiesClimateResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *climate = static_cast(entity); @@ -719,11 +707,7 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection return encode_message_to_buffer(msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } void APIConnection::climate_command(const ClimateCommandRequest &msg) { - climate::Climate *climate = App.get_climate_by_key(msg.key); - if (climate == nullptr) - return; - - auto call = climate->make_call(); + ENTITY_COMMAND_MAKE_CALL(climate::Climate, climate, climate) if (msg.has_mode) call.set_mode(static_cast(msg.mode)); if (msg.has_target_temperature) @@ -750,10 +734,7 @@ void APIConnection::climate_command(const ClimateCommandRequest &msg) { #ifdef USE_NUMBER bool APIConnection::send_number_state(number::Number *number) { - return this->schedule_message_(number, &APIConnection::try_send_number_state, NumberStateResponse::MESSAGE_TYPE); -} -void APIConnection::send_number_info(number::Number *number) { - this->schedule_message_(number, &APIConnection::try_send_number_info, ListEntitiesNumberResponse::MESSAGE_TYPE); + return this->send_message_smart_(number, &APIConnection::try_send_number_state, NumberStateResponse::MESSAGE_TYPE); } uint16_t APIConnection::try_send_number_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, @@ -781,11 +762,7 @@ uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection * return encode_message_to_buffer(msg, ListEntitiesNumberResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } void APIConnection::number_command(const NumberCommandRequest &msg) { - number::Number *number = App.get_number_by_key(msg.key); - if (number == nullptr) - return; - - auto call = number->make_call(); + ENTITY_COMMAND_MAKE_CALL(number::Number, number, number) call.set_value(msg.state); call.perform(); } @@ -793,7 +770,7 @@ void APIConnection::number_command(const NumberCommandRequest &msg) { #ifdef USE_DATETIME_DATE bool APIConnection::send_date_state(datetime::DateEntity *date) { - return this->schedule_message_(date, &APIConnection::try_send_date_state, DateStateResponse::MESSAGE_TYPE); + return this->send_message_smart_(date, &APIConnection::try_send_date_state, DateStateResponse::MESSAGE_TYPE); } uint16_t APIConnection::try_send_date_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -806,9 +783,6 @@ uint16_t APIConnection::try_send_date_state(EntityBase *entity, APIConnection *c fill_entity_state_base(date, resp); return encode_message_to_buffer(resp, DateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } -void APIConnection::send_date_info(datetime::DateEntity *date) { - this->schedule_message_(date, &APIConnection::try_send_date_info, ListEntitiesDateResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_date_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *date = static_cast(entity); @@ -818,11 +792,7 @@ uint16_t APIConnection::try_send_date_info(EntityBase *entity, APIConnection *co return encode_message_to_buffer(msg, ListEntitiesDateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } void APIConnection::date_command(const DateCommandRequest &msg) { - datetime::DateEntity *date = App.get_date_by_key(msg.key); - if (date == nullptr) - return; - - auto call = date->make_call(); + ENTITY_COMMAND_MAKE_CALL(datetime::DateEntity, date, date) call.set_date(msg.year, msg.month, msg.day); call.perform(); } @@ -830,7 +800,7 @@ void APIConnection::date_command(const DateCommandRequest &msg) { #ifdef USE_DATETIME_TIME bool APIConnection::send_time_state(datetime::TimeEntity *time) { - return this->schedule_message_(time, &APIConnection::try_send_time_state, TimeStateResponse::MESSAGE_TYPE); + return this->send_message_smart_(time, &APIConnection::try_send_time_state, TimeStateResponse::MESSAGE_TYPE); } uint16_t APIConnection::try_send_time_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -843,9 +813,6 @@ uint16_t APIConnection::try_send_time_state(EntityBase *entity, APIConnection *c fill_entity_state_base(time, resp); return encode_message_to_buffer(resp, TimeStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } -void APIConnection::send_time_info(datetime::TimeEntity *time) { - this->schedule_message_(time, &APIConnection::try_send_time_info, ListEntitiesTimeResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_time_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *time = static_cast(entity); @@ -855,11 +822,7 @@ uint16_t APIConnection::try_send_time_info(EntityBase *entity, APIConnection *co return encode_message_to_buffer(msg, ListEntitiesTimeResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } void APIConnection::time_command(const TimeCommandRequest &msg) { - datetime::TimeEntity *time = App.get_time_by_key(msg.key); - if (time == nullptr) - return; - - auto call = time->make_call(); + ENTITY_COMMAND_MAKE_CALL(datetime::TimeEntity, time, time) call.set_time(msg.hour, msg.minute, msg.second); call.perform(); } @@ -867,8 +830,8 @@ void APIConnection::time_command(const TimeCommandRequest &msg) { #ifdef USE_DATETIME_DATETIME bool APIConnection::send_datetime_state(datetime::DateTimeEntity *datetime) { - return this->schedule_message_(datetime, &APIConnection::try_send_datetime_state, - DateTimeStateResponse::MESSAGE_TYPE); + return this->send_message_smart_(datetime, &APIConnection::try_send_datetime_state, + DateTimeStateResponse::MESSAGE_TYPE); } uint16_t APIConnection::try_send_datetime_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -882,9 +845,6 @@ uint16_t APIConnection::try_send_datetime_state(EntityBase *entity, APIConnectio fill_entity_state_base(datetime, resp); return encode_message_to_buffer(resp, DateTimeStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } -void APIConnection::send_datetime_info(datetime::DateTimeEntity *datetime) { - this->schedule_message_(datetime, &APIConnection::try_send_datetime_info, ListEntitiesDateTimeResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_datetime_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *datetime = static_cast(entity); @@ -894,11 +854,7 @@ uint16_t APIConnection::try_send_datetime_info(EntityBase *entity, APIConnection return encode_message_to_buffer(msg, ListEntitiesDateTimeResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } void APIConnection::datetime_command(const DateTimeCommandRequest &msg) { - datetime::DateTimeEntity *datetime = App.get_datetime_by_key(msg.key); - if (datetime == nullptr) - return; - - auto call = datetime->make_call(); + ENTITY_COMMAND_MAKE_CALL(datetime::DateTimeEntity, datetime, datetime) call.set_datetime(msg.epoch_seconds); call.perform(); } @@ -906,10 +862,7 @@ void APIConnection::datetime_command(const DateTimeCommandRequest &msg) { #ifdef USE_TEXT bool APIConnection::send_text_state(text::Text *text) { - return this->schedule_message_(text, &APIConnection::try_send_text_state, TextStateResponse::MESSAGE_TYPE); -} -void APIConnection::send_text_info(text::Text *text) { - this->schedule_message_(text, &APIConnection::try_send_text_info, ListEntitiesTextResponse::MESSAGE_TYPE); + return this->send_message_smart_(text, &APIConnection::try_send_text_state, TextStateResponse::MESSAGE_TYPE); } uint16_t APIConnection::try_send_text_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, @@ -935,11 +888,7 @@ uint16_t APIConnection::try_send_text_info(EntityBase *entity, APIConnection *co return encode_message_to_buffer(msg, ListEntitiesTextResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } void APIConnection::text_command(const TextCommandRequest &msg) { - text::Text *text = App.get_text_by_key(msg.key); - if (text == nullptr) - return; - - auto call = text->make_call(); + ENTITY_COMMAND_MAKE_CALL(text::Text, text, text) call.set_value(msg.state); call.perform(); } @@ -947,10 +896,7 @@ void APIConnection::text_command(const TextCommandRequest &msg) { #ifdef USE_SELECT bool APIConnection::send_select_state(select::Select *select) { - return this->schedule_message_(select, &APIConnection::try_send_select_state, SelectStateResponse::MESSAGE_TYPE); -} -void APIConnection::send_select_info(select::Select *select) { - this->schedule_message_(select, &APIConnection::try_send_select_info, ListEntitiesSelectResponse::MESSAGE_TYPE); + return this->send_message_smart_(select, &APIConnection::try_send_select_state, SelectStateResponse::MESSAGE_TYPE); } uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, @@ -974,20 +920,13 @@ uint16_t APIConnection::try_send_select_info(EntityBase *entity, APIConnection * return encode_message_to_buffer(msg, ListEntitiesSelectResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } void APIConnection::select_command(const SelectCommandRequest &msg) { - select::Select *select = App.get_select_by_key(msg.key); - if (select == nullptr) - return; - - auto call = select->make_call(); + ENTITY_COMMAND_MAKE_CALL(select::Select, select, select) call.set_option(msg.state); call.perform(); } #endif #ifdef USE_BUTTON -void esphome::api::APIConnection::send_button_info(button::Button *button) { - this->schedule_message_(button, &APIConnection::try_send_button_info, ListEntitiesButtonResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_button_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *button = static_cast(entity); @@ -998,20 +937,14 @@ uint16_t APIConnection::try_send_button_info(EntityBase *entity, APIConnection * return encode_message_to_buffer(msg, ListEntitiesButtonResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } void esphome::api::APIConnection::button_command(const ButtonCommandRequest &msg) { - button::Button *button = App.get_button_by_key(msg.key); - if (button == nullptr) - return; - + ENTITY_COMMAND_GET(button::Button, button, button) button->press(); } #endif #ifdef USE_LOCK bool APIConnection::send_lock_state(lock::Lock *a_lock) { - return this->schedule_message_(a_lock, &APIConnection::try_send_lock_state, LockStateResponse::MESSAGE_TYPE); -} -void APIConnection::send_lock_info(lock::Lock *a_lock) { - this->schedule_message_(a_lock, &APIConnection::try_send_lock_info, ListEntitiesLockResponse::MESSAGE_TYPE); + return this->send_message_smart_(a_lock, &APIConnection::try_send_lock_state, LockStateResponse::MESSAGE_TYPE); } uint16_t APIConnection::try_send_lock_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, @@ -1035,9 +968,7 @@ uint16_t APIConnection::try_send_lock_info(EntityBase *entity, APIConnection *co return encode_message_to_buffer(msg, ListEntitiesLockResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } void APIConnection::lock_command(const LockCommandRequest &msg) { - lock::Lock *a_lock = App.get_lock_by_key(msg.key); - if (a_lock == nullptr) - return; + ENTITY_COMMAND_GET(lock::Lock, a_lock, lock) switch (msg.command) { case enums::LOCK_UNLOCK: @@ -1055,7 +986,7 @@ void APIConnection::lock_command(const LockCommandRequest &msg) { #ifdef USE_VALVE bool APIConnection::send_valve_state(valve::Valve *valve) { - return this->schedule_message_(valve, &APIConnection::try_send_valve_state, ValveStateResponse::MESSAGE_TYPE); + return this->send_message_smart_(valve, &APIConnection::try_send_valve_state, ValveStateResponse::MESSAGE_TYPE); } uint16_t APIConnection::try_send_valve_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -1066,9 +997,6 @@ uint16_t APIConnection::try_send_valve_state(EntityBase *entity, APIConnection * fill_entity_state_base(valve, resp); return encode_message_to_buffer(resp, ValveStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } -void APIConnection::send_valve_info(valve::Valve *valve) { - this->schedule_message_(valve, &APIConnection::try_send_valve_info, ListEntitiesValveResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *valve = static_cast(entity); @@ -1083,11 +1011,7 @@ uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *c return encode_message_to_buffer(msg, ListEntitiesValveResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } void APIConnection::valve_command(const ValveCommandRequest &msg) { - valve::Valve *valve = App.get_valve_by_key(msg.key); - if (valve == nullptr) - return; - - auto call = valve->make_call(); + ENTITY_COMMAND_MAKE_CALL(valve::Valve, valve, valve) if (msg.has_position) call.set_position(msg.position); if (msg.stop) @@ -1098,8 +1022,8 @@ void APIConnection::valve_command(const ValveCommandRequest &msg) { #ifdef USE_MEDIA_PLAYER bool APIConnection::send_media_player_state(media_player::MediaPlayer *media_player) { - return this->schedule_message_(media_player, &APIConnection::try_send_media_player_state, - MediaPlayerStateResponse::MESSAGE_TYPE); + return this->send_message_smart_(media_player, &APIConnection::try_send_media_player_state, + MediaPlayerStateResponse::MESSAGE_TYPE); } uint16_t APIConnection::try_send_media_player_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -1114,10 +1038,6 @@ uint16_t APIConnection::try_send_media_player_state(EntityBase *entity, APIConne fill_entity_state_base(media_player, resp); return encode_message_to_buffer(resp, MediaPlayerStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } -void APIConnection::send_media_player_info(media_player::MediaPlayer *media_player) { - this->schedule_message_(media_player, &APIConnection::try_send_media_player_info, - ListEntitiesMediaPlayerResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *media_player = static_cast(entity); @@ -1138,11 +1058,7 @@ uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnec return encode_message_to_buffer(msg, ListEntitiesMediaPlayerResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) { - media_player::MediaPlayer *media_player = App.get_media_player_by_key(msg.key); - if (media_player == nullptr) - return; - - auto call = media_player->make_call(); + ENTITY_COMMAND_MAKE_CALL(media_player::MediaPlayer, media_player, media_player) if (msg.has_command) { call.set_command(static_cast(msg.command)); } @@ -1159,39 +1075,36 @@ void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) { } #endif -#ifdef USE_ESP32_CAMERA -void APIConnection::set_camera_state(std::shared_ptr image) { - if (!this->state_subscription_) +#ifdef USE_CAMERA +void APIConnection::set_camera_state(std::shared_ptr image) { + if (!this->flags_.state_subscription) return; - if (this->image_reader_.available()) + if (!this->image_reader_) return; - if (image->was_requested_by(esphome::esp32_camera::API_REQUESTER) || - image->was_requested_by(esphome::esp32_camera::IDLE)) - this->image_reader_.set_image(std::move(image)); -} -void APIConnection::send_camera_info(esp32_camera::ESP32Camera *camera) { - this->schedule_message_(camera, &APIConnection::try_send_camera_info, ListEntitiesCameraResponse::MESSAGE_TYPE); + if (this->image_reader_->available()) + return; + if (image->was_requested_by(esphome::camera::API_REQUESTER) || image->was_requested_by(esphome::camera::IDLE)) + this->image_reader_->set_image(std::move(image)); } uint16_t APIConnection::try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { - auto *camera = static_cast(entity); + auto *camera = static_cast(entity); ListEntitiesCameraResponse msg; msg.unique_id = get_default_unique_id("camera", camera); fill_entity_info_base(camera, msg); return encode_message_to_buffer(msg, ListEntitiesCameraResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } void APIConnection::camera_image(const CameraImageRequest &msg) { - if (esp32_camera::global_esp32_camera == nullptr) + if (camera::Camera::instance() == nullptr) return; if (msg.single) - esp32_camera::global_esp32_camera->request_image(esphome::esp32_camera::API_REQUESTER); + camera::Camera::instance()->request_image(esphome::camera::API_REQUESTER); if (msg.stream) { - esp32_camera::global_esp32_camera->start_stream(esphome::esp32_camera::API_REQUESTER); + camera::Camera::instance()->start_stream(esphome::camera::API_REQUESTER); - App.scheduler.set_timeout(this->parent_, "api_esp32_camera_stop_stream", ESP32_CAMERA_STOP_STREAM, []() { - esp32_camera::global_esp32_camera->stop_stream(esphome::esp32_camera::API_REQUESTER); - }); + App.scheduler.set_timeout(this->parent_, "api_camera_stop_stream", CAMERA_STOP_STREAM, + []() { camera::Camera::instance()->stop_stream(esphome::camera::API_REQUESTER); }); } } #endif @@ -1263,66 +1176,53 @@ void APIConnection::bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequ #endif #ifdef USE_VOICE_ASSISTANT +bool APIConnection::check_voice_assistant_api_connection_() const { + return voice_assistant::global_voice_assistant != nullptr && + voice_assistant::global_voice_assistant->get_api_connection() == this; +} + void APIConnection::subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) { if (voice_assistant::global_voice_assistant != nullptr) { voice_assistant::global_voice_assistant->client_subscription(this, msg.subscribe); } } void APIConnection::on_voice_assistant_response(const VoiceAssistantResponse &msg) { - if (voice_assistant::global_voice_assistant != nullptr) { - if (voice_assistant::global_voice_assistant->get_api_connection() != this) { - return; - } + if (!this->check_voice_assistant_api_connection_()) { + return; + } - if (msg.error) { - voice_assistant::global_voice_assistant->failed_to_start(); - return; - } - if (msg.port == 0) { - // Use API Audio - voice_assistant::global_voice_assistant->start_streaming(); - } else { - struct sockaddr_storage storage; - socklen_t len = sizeof(storage); - this->helper_->getpeername((struct sockaddr *) &storage, &len); - voice_assistant::global_voice_assistant->start_streaming(&storage, msg.port); - } + if (msg.error) { + voice_assistant::global_voice_assistant->failed_to_start(); + return; + } + if (msg.port == 0) { + // Use API Audio + voice_assistant::global_voice_assistant->start_streaming(); + } else { + struct sockaddr_storage storage; + socklen_t len = sizeof(storage); + this->helper_->getpeername((struct sockaddr *) &storage, &len); + voice_assistant::global_voice_assistant->start_streaming(&storage, msg.port); } }; void APIConnection::on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg) { - if (voice_assistant::global_voice_assistant != nullptr) { - if (voice_assistant::global_voice_assistant->get_api_connection() != this) { - return; - } - + if (this->check_voice_assistant_api_connection_()) { voice_assistant::global_voice_assistant->on_event(msg); } } void APIConnection::on_voice_assistant_audio(const VoiceAssistantAudio &msg) { - if (voice_assistant::global_voice_assistant != nullptr) { - if (voice_assistant::global_voice_assistant->get_api_connection() != this) { - return; - } - + if (this->check_voice_assistant_api_connection_()) { voice_assistant::global_voice_assistant->on_audio(msg); } }; void APIConnection::on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &msg) { - if (voice_assistant::global_voice_assistant != nullptr) { - if (voice_assistant::global_voice_assistant->get_api_connection() != this) { - return; - } - + if (this->check_voice_assistant_api_connection_()) { voice_assistant::global_voice_assistant->on_timer_event(msg); } }; void APIConnection::on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &msg) { - if (voice_assistant::global_voice_assistant != nullptr) { - if (voice_assistant::global_voice_assistant->get_api_connection() != this) { - return; - } - + if (this->check_voice_assistant_api_connection_()) { voice_assistant::global_voice_assistant->on_announce(msg); } } @@ -1330,35 +1230,29 @@ void APIConnection::on_voice_assistant_announce_request(const VoiceAssistantAnno VoiceAssistantConfigurationResponse APIConnection::voice_assistant_get_configuration( const VoiceAssistantConfigurationRequest &msg) { VoiceAssistantConfigurationResponse resp; - if (voice_assistant::global_voice_assistant != nullptr) { - if (voice_assistant::global_voice_assistant->get_api_connection() != this) { - return resp; - } - - auto &config = voice_assistant::global_voice_assistant->get_configuration(); - for (auto &wake_word : config.available_wake_words) { - VoiceAssistantWakeWord resp_wake_word; - resp_wake_word.id = wake_word.id; - resp_wake_word.wake_word = wake_word.wake_word; - for (const auto &lang : wake_word.trained_languages) { - resp_wake_word.trained_languages.push_back(lang); - } - resp.available_wake_words.push_back(std::move(resp_wake_word)); - } - for (auto &wake_word_id : config.active_wake_words) { - resp.active_wake_words.push_back(wake_word_id); - } - resp.max_active_wake_words = config.max_active_wake_words; + if (!this->check_voice_assistant_api_connection_()) { + return resp; } + + auto &config = voice_assistant::global_voice_assistant->get_configuration(); + for (auto &wake_word : config.available_wake_words) { + VoiceAssistantWakeWord resp_wake_word; + resp_wake_word.id = wake_word.id; + resp_wake_word.wake_word = wake_word.wake_word; + for (const auto &lang : wake_word.trained_languages) { + resp_wake_word.trained_languages.push_back(lang); + } + resp.available_wake_words.push_back(std::move(resp_wake_word)); + } + for (auto &wake_word_id : config.active_wake_words) { + resp.active_wake_words.push_back(wake_word_id); + } + resp.max_active_wake_words = config.max_active_wake_words; return resp; } void APIConnection::voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) { - if (voice_assistant::global_voice_assistant != nullptr) { - if (voice_assistant::global_voice_assistant->get_api_connection() != this) { - return; - } - + if (this->check_voice_assistant_api_connection_()) { voice_assistant::global_voice_assistant->on_set_configuration(msg.active_wake_words); } } @@ -1367,8 +1261,8 @@ void APIConnection::voice_assistant_set_configuration(const VoiceAssistantSetCon #ifdef USE_ALARM_CONTROL_PANEL bool APIConnection::send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { - return this->schedule_message_(a_alarm_control_panel, &APIConnection::try_send_alarm_control_panel_state, - AlarmControlPanelStateResponse::MESSAGE_TYPE); + return this->send_message_smart_(a_alarm_control_panel, &APIConnection::try_send_alarm_control_panel_state, + AlarmControlPanelStateResponse::MESSAGE_TYPE); } uint16_t APIConnection::try_send_alarm_control_panel_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -1378,10 +1272,6 @@ uint16_t APIConnection::try_send_alarm_control_panel_state(EntityBase *entity, A fill_entity_state_base(a_alarm_control_panel, resp); return encode_message_to_buffer(resp, AlarmControlPanelStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } -void APIConnection::send_alarm_control_panel_info(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { - this->schedule_message_(a_alarm_control_panel, &APIConnection::try_send_alarm_control_panel_info, - ListEntitiesAlarmControlPanelResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_alarm_control_panel_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *a_alarm_control_panel = static_cast(entity); @@ -1395,11 +1285,7 @@ uint16_t APIConnection::try_send_alarm_control_panel_info(EntityBase *entity, AP is_single); } void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) { - alarm_control_panel::AlarmControlPanel *a_alarm_control_panel = App.get_alarm_control_panel_by_key(msg.key); - if (a_alarm_control_panel == nullptr) - return; - - auto call = a_alarm_control_panel->make_call(); + ENTITY_COMMAND_MAKE_CALL(alarm_control_panel::AlarmControlPanel, a_alarm_control_panel, alarm_control_panel) switch (msg.command) { case enums::ALARM_CONTROL_PANEL_DISARM: call.disarm(); @@ -1430,10 +1316,7 @@ void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRe #ifdef USE_EVENT void APIConnection::send_event(event::Event *event, const std::string &event_type) { - this->schedule_message_(event, MessageCreator(event_type, EventResponse::MESSAGE_TYPE), EventResponse::MESSAGE_TYPE); -} -void APIConnection::send_event_info(event::Event *event) { - this->schedule_message_(event, &APIConnection::try_send_event_info, ListEntitiesEventResponse::MESSAGE_TYPE); + this->schedule_message_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE); } uint16_t APIConnection::try_send_event_response(event::Event *event, const std::string &event_type, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -1458,7 +1341,7 @@ uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *c #ifdef USE_UPDATE bool APIConnection::send_update_state(update::UpdateEntity *update) { - return this->schedule_message_(update, &APIConnection::try_send_update_state, UpdateStateResponse::MESSAGE_TYPE); + return this->send_message_smart_(update, &APIConnection::try_send_update_state, UpdateStateResponse::MESSAGE_TYPE); } uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -1480,9 +1363,6 @@ uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection fill_entity_state_base(update, resp); return encode_message_to_buffer(resp, UpdateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } -void APIConnection::send_update_info(update::UpdateEntity *update) { - this->schedule_message_(update, &APIConnection::try_send_update_info, ListEntitiesUpdateResponse::MESSAGE_TYPE); -} uint16_t APIConnection::try_send_update_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *update = static_cast(entity); @@ -1493,9 +1373,7 @@ uint16_t APIConnection::try_send_update_info(EntityBase *entity, APIConnection * return encode_message_to_buffer(msg, ListEntitiesUpdateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } void APIConnection::update_command(const UpdateCommandRequest &msg) { - update::UpdateEntity *update = App.get_update_by_key(msg.key); - if (update == nullptr) - return; + ENTITY_COMMAND_GET(update::UpdateEntity, update, update) switch (msg.command) { case enums::UPDATE_COMMAND_UPDATE: @@ -1514,12 +1392,11 @@ void APIConnection::update_command(const UpdateCommandRequest &msg) { } #endif -bool APIConnection::try_send_log_message(int level, const char *tag, const char *line) { - if (this->log_subscription_ < level) +bool APIConnection::try_send_log_message(int level, const char *tag, const char *line, size_t message_len) { + if (this->flags_.log_subscription < level) return false; // Pre-calculate message size to avoid reallocations - const size_t line_length = strlen(line); uint32_t msg_size = 0; // Add size for level field (field ID 1, varint type) @@ -1528,14 +1405,14 @@ bool APIConnection::try_send_log_message(int level, const char *tag, const char // Add size for string field (field ID 3, string type) // 1 byte for field tag + size of length varint + string length - msg_size += 1 + api::ProtoSize::varint(static_cast(line_length)) + line_length; + msg_size += 1 + api::ProtoSize::varint(static_cast(message_len)) + message_len; // Create a pre-sized buffer auto buffer = this->create_buffer(msg_size); // Encode the message (SubscribeLogsResponse) buffer.encode_uint32(1, static_cast(level)); // LogLevel level = 1 - buffer.encode_string(3, line, line_length); // string message = 3 + buffer.encode_string(3, line, message_len); // string message = 3 // SubscribeLogsResponse - 29 return this->send_buffer(buffer, SubscribeLogsResponse::MESSAGE_TYPE); @@ -1544,8 +1421,7 @@ bool APIConnection::try_send_log_message(int level, const char *tag, const char HelloResponse APIConnection::hello(const HelloRequest &msg) { this->client_info_ = msg.client_info; this->client_peername_ = this->helper_->getpeername(); - this->client_combined_info_ = this->client_info_ + " (" + this->client_peername_ + ")"; - this->helper_->set_log_info(this->client_combined_info_); + this->helper_->set_log_info(this->get_client_combined_info()); this->client_api_version_major_ = msg.api_version_major; this->client_api_version_minor_ = msg.api_version_minor; ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->client_info_.c_str(), @@ -1557,19 +1433,24 @@ HelloResponse APIConnection::hello(const HelloRequest &msg) { resp.server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")"; resp.name = App.get_name(); - this->connection_state_ = ConnectionState::CONNECTED; + this->flags_.connection_state = static_cast(ConnectionState::CONNECTED); return resp; } ConnectResponse APIConnection::connect(const ConnectRequest &msg) { - bool correct = this->parent_->check_password(msg.password); + bool correct = true; +#ifdef USE_API_PASSWORD + correct = this->parent_->check_password(msg.password); +#endif ConnectResponse resp; // bool invalid_password = 1; resp.invalid_password = !correct; if (correct) { - ESP_LOGD(TAG, "%s connected", this->client_combined_info_.c_str()); - this->connection_state_ = ConnectionState::AUTHENTICATED; + ESP_LOGD(TAG, "%s connected", this->get_client_combined_info().c_str()); + this->flags_.connection_state = static_cast(ConnectionState::AUTHENTICATED); +#ifdef USE_API_CLIENT_CONNECTED_TRIGGER this->parent_->get_client_connected_trigger()->trigger(this->client_info_, this->client_peername_); +#endif #ifdef USE_HOMEASSISTANT_TIME if (homeassistant::global_homeassistant_time != nullptr) { this->send_time_request(); @@ -1580,7 +1461,11 @@ ConnectResponse APIConnection::connect(const ConnectRequest &msg) { } DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { DeviceInfoResponse resp{}; +#ifdef USE_API_PASSWORD resp.uses_password = this->parent_->uses_password(); +#else + resp.uses_password = false; +#endif resp.name = App.get_name(); resp.friendly_name = App.get_friendly_name(); resp.suggested_area = App.get_area(); @@ -1593,6 +1478,8 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { resp.manufacturer = "Raspberry Pi"; #elif defined(USE_BK72XX) resp.manufacturer = "Beken"; +#elif defined(USE_LN882X) + resp.manufacturer = "Lightning"; #elif defined(USE_RTL87XX) resp.manufacturer = "Realtek"; #elif defined(USE_HOST) @@ -1620,6 +1507,23 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { #endif #ifdef USE_API_NOISE resp.api_encryption_supported = true; +#endif +#ifdef USE_DEVICES + for (auto const &device : App.get_devices()) { + DeviceInfo device_info; + device_info.device_id = device->get_device_id(); + device_info.name = device->get_name(); + device_info.area_id = device->get_area_id(); + resp.devices.push_back(device_info); + } +#endif +#ifdef USE_AREAS + for (auto const &area : App.get_areas()) { + AreaInfo area_info; + area_info.area_id = area->get_area_id(); + area_info.name = area->get_name(); + resp.areas.push_back(area_info); + } #endif return resp; } @@ -1665,7 +1569,7 @@ void APIConnection::subscribe_home_assistant_states(const SubscribeHomeAssistant state_subs_at_ = 0; } bool APIConnection::try_to_clear_buffer(bool log_out_of_space) { - if (this->remove_) + if (this->flags_.remove) return false; if (this->helper_->can_write_without_blocking()) return true; @@ -1673,7 +1577,7 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) { APIError err = this->helper_->loop(); if (err != APIError::OK) { on_fatal_error(); - ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->client_combined_info_.c_str(), + ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->get_client_combined_info().c_str(), api_error_to_str(err), errno); return false; } @@ -1695,10 +1599,10 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint16_t message_type) if (err != APIError::OK) { on_fatal_error(); if (err == APIError::SOCKET_WRITE_FAILED && errno == ECONNRESET) { - ESP_LOGW(TAG, "%s: Connection reset", this->client_combined_info_.c_str()); + ESP_LOGW(TAG, "%s: Connection reset", this->get_client_combined_info().c_str()); } else { - ESP_LOGW(TAG, "%s: Packet write failed %s errno=%d", this->client_combined_info_.c_str(), api_error_to_str(err), - errno); + ESP_LOGW(TAG, "%s: Packet write failed %s errno=%d", this->get_client_combined_info().c_str(), + api_error_to_str(err), errno); } return false; } @@ -1707,15 +1611,15 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint16_t message_type) } void APIConnection::on_unauthenticated_access() { this->on_fatal_error(); - ESP_LOGD(TAG, "%s requested access without authentication", this->client_combined_info_.c_str()); + ESP_LOGD(TAG, "%s requested access without authentication", this->get_client_combined_info().c_str()); } void APIConnection::on_no_setup_connection() { this->on_fatal_error(); - ESP_LOGD(TAG, "%s requested access without full connection", this->client_combined_info_.c_str()); + ESP_LOGD(TAG, "%s requested access without full connection", this->get_client_combined_info().c_str()); } void APIConnection::on_fatal_error() { this->helper_->close(); - this->remove_ = true; + this->flags_.remove = true; } void APIConnection::DeferredBatch::add_item(EntityBase *entity, MessageCreator creator, uint16_t message_type) { @@ -1724,7 +1628,9 @@ void APIConnection::DeferredBatch::add_item(EntityBase *entity, MessageCreator c // O(n) but optimized for RAM and not performance. for (auto &item : items) { if (item.entity == entity && item.message_type == message_type) { - // Update the existing item with the new creator + // Clean up old creator before replacing + item.creator.cleanup(message_type); + // Move assign the new creator item.creator = std::move(creator); return; } @@ -1734,9 +1640,14 @@ void APIConnection::DeferredBatch::add_item(EntityBase *entity, MessageCreator c items.emplace_back(entity, std::move(creator), message_type); } +void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, MessageCreator creator, uint16_t message_type) { + // Insert at front for high priority messages (no deduplication check) + items.insert(items.begin(), BatchItem(entity, std::move(creator), message_type)); +} + bool APIConnection::schedule_batch_() { - if (!this->deferred_batch_.batch_scheduled) { - this->deferred_batch_.batch_scheduled = true; + if (!this->flags_.batch_scheduled) { + this->flags_.batch_scheduled = true; this->deferred_batch_.batch_start_time = App.get_loop_component_start_time(); } return true; @@ -1745,14 +1656,14 @@ bool APIConnection::schedule_batch_() { ProtoWriteBuffer APIConnection::allocate_single_message_buffer(uint16_t size) { return this->create_buffer(size); } ProtoWriteBuffer APIConnection::allocate_batch_message_buffer(uint16_t size) { - ProtoWriteBuffer result = this->prepare_message_buffer(size, this->batch_first_message_); - this->batch_first_message_ = false; + ProtoWriteBuffer result = this->prepare_message_buffer(size, this->flags_.batch_first_message); + this->flags_.batch_first_message = false; return result; } void APIConnection::process_batch_() { if (this->deferred_batch_.empty()) { - this->deferred_batch_.batch_scheduled = false; + this->flags_.batch_scheduled = false; return; } @@ -1762,22 +1673,28 @@ void APIConnection::process_batch_() { return; } - size_t num_items = this->deferred_batch_.items.size(); + size_t num_items = this->deferred_batch_.size(); // Fast path for single message - allocate exact size needed if (num_items == 1) { - const auto &item = this->deferred_batch_.items[0]; + const auto &item = this->deferred_batch_[0]; // Let the creator calculate size and encode if it fits - uint16_t payload_size = item.creator(item.entity, this, std::numeric_limits::max(), true); + uint16_t payload_size = + item.creator(item.entity, this, std::numeric_limits::max(), true, item.message_type); if (payload_size > 0 && this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, item.message_type)) { - this->deferred_batch_.clear(); +#ifdef HAS_PROTO_MESSAGE_DUMP + // Log messages after send attempt for VV debugging + // It's safe to use the buffer for logging at this point regardless of send result + this->log_batch_item_(item); +#endif + this->clear_batch_(); } else if (payload_size == 0) { // Message too large ESP_LOGW(TAG, "Message too large to send: type=%u", item.message_type); - this->deferred_batch_.clear(); + this->clear_batch_(); } return; } @@ -1795,7 +1712,8 @@ void APIConnection::process_batch_() { // Pre-calculate exact buffer size needed based on message types uint32_t total_estimated_size = 0; - for (const auto &item : this->deferred_batch_.items) { + for (size_t i = 0; i < this->deferred_batch_.size(); i++) { + const auto &item = this->deferred_batch_[i]; total_estimated_size += get_estimated_message_size(item.message_type); } @@ -1804,7 +1722,7 @@ void APIConnection::process_batch_() { // Reserve based on estimated size (much more accurate than 24-byte worst-case) this->parent_->get_shared_buffer_ref().reserve(total_estimated_size + total_overhead); - this->batch_first_message_ = true; + this->flags_.batch_first_message = true; size_t items_processed = 0; uint16_t remaining_size = std::numeric_limits::max(); @@ -1816,10 +1734,11 @@ void APIConnection::process_batch_() { uint32_t current_offset = 0; // Process items and encode directly to buffer - for (const auto &item : this->deferred_batch_.items) { + for (size_t i = 0; i < this->deferred_batch_.size(); i++) { + const auto &item = this->deferred_batch_[i]; // Try to encode message // The creator will calculate overhead to determine if the message fits - uint16_t payload_size = item.creator(item.entity, this, remaining_size, false); + uint16_t payload_size = item.creator(item.entity, this, remaining_size, false, item.message_type); if (payload_size == 0) { // Message won't fit, stop processing @@ -1860,44 +1779,46 @@ void APIConnection::process_batch_() { if (err != APIError::OK && err != APIError::WOULD_BLOCK) { on_fatal_error(); if (err == APIError::SOCKET_WRITE_FAILED && errno == ECONNRESET) { - ESP_LOGW(TAG, "%s: Connection reset during batch write", this->client_combined_info_.c_str()); + ESP_LOGW(TAG, "%s: Connection reset during batch write", this->get_client_combined_info().c_str()); } else { - ESP_LOGW(TAG, "%s: Batch write failed %s errno=%d", this->client_combined_info_.c_str(), api_error_to_str(err), - errno); + ESP_LOGW(TAG, "%s: Batch write failed %s errno=%d", this->get_client_combined_info().c_str(), + api_error_to_str(err), errno); } } - // Handle remaining items more efficiently - if (items_processed < this->deferred_batch_.items.size()) { - // Remove processed items from the beginning - this->deferred_batch_.items.erase(this->deferred_batch_.items.begin(), - this->deferred_batch_.items.begin() + items_processed); +#ifdef HAS_PROTO_MESSAGE_DUMP + // Log messages after send attempt for VV debugging + // It's safe to use the buffer for logging at this point regardless of send result + for (size_t i = 0; i < items_processed; i++) { + const auto &item = this->deferred_batch_[i]; + this->log_batch_item_(item); + } +#endif + // Handle remaining items more efficiently + if (items_processed < this->deferred_batch_.size()) { + // Remove processed items from the beginning with proper cleanup + this->deferred_batch_.remove_front(items_processed); // Reschedule for remaining items this->schedule_batch_(); } else { // All items processed - this->deferred_batch_.clear(); + this->clear_batch_(); } } uint16_t APIConnection::MessageCreator::operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) const { - switch (message_type_) { - case 0: // Function pointer - return data_.ptr(entity, conn, remaining_size, is_single); - + bool is_single, uint16_t message_type) const { #ifdef USE_EVENT - case EventResponse::MESSAGE_TYPE: { - auto *e = static_cast(entity); - return APIConnection::try_send_event_response(e, *data_.string_ptr, conn, remaining_size, is_single); - } + // Special case: EventResponse uses string pointer + if (message_type == EventResponse::MESSAGE_TYPE) { + auto *e = static_cast(entity); + return APIConnection::try_send_event_response(e, *data_.string_ptr, conn, remaining_size, is_single); + } #endif - default: - // Should not happen, return 0 to indicate no message - return 0; - } + // All other message types use function pointers + return data_.function_ptr(entity, conn, remaining_size, is_single); } uint16_t APIConnection::try_send_list_info_done(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, @@ -1912,6 +1833,12 @@ uint16_t APIConnection::try_send_disconnect_request(EntityBase *entity, APIConne return encode_message_to_buffer(req, DisconnectRequest::MESSAGE_TYPE, conn, remaining_size, is_single); } +uint16_t APIConnection::try_send_ping_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, + bool is_single) { + PingRequest req; + 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) { diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 7cd41561d4..b70b037999 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -18,10 +18,13 @@ namespace api { // Keepalive timeout in milliseconds static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000; +// Maximum number of entities to process in a single batch during initial state/info sending +static constexpr size_t MAX_INITIAL_PER_BATCH = 20; class APIConnection : public APIServerConnection { public: friend class APIServer; + friend class ListEntitiesIterator; APIConnection(std::unique_ptr socket, APIServer *parent); virtual ~APIConnection(); @@ -34,98 +37,79 @@ class APIConnection : public APIServerConnection { } #ifdef USE_BINARY_SENSOR bool send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor); - void send_binary_sensor_info(binary_sensor::BinarySensor *binary_sensor); #endif #ifdef USE_COVER bool send_cover_state(cover::Cover *cover); - void send_cover_info(cover::Cover *cover); void cover_command(const CoverCommandRequest &msg) override; #endif #ifdef USE_FAN bool send_fan_state(fan::Fan *fan); - void send_fan_info(fan::Fan *fan); void fan_command(const FanCommandRequest &msg) override; #endif #ifdef USE_LIGHT bool send_light_state(light::LightState *light); - void send_light_info(light::LightState *light); void light_command(const LightCommandRequest &msg) override; #endif #ifdef USE_SENSOR bool send_sensor_state(sensor::Sensor *sensor); - void send_sensor_info(sensor::Sensor *sensor); #endif #ifdef USE_SWITCH bool send_switch_state(switch_::Switch *a_switch); - void send_switch_info(switch_::Switch *a_switch); void switch_command(const SwitchCommandRequest &msg) override; #endif #ifdef USE_TEXT_SENSOR bool send_text_sensor_state(text_sensor::TextSensor *text_sensor); - void send_text_sensor_info(text_sensor::TextSensor *text_sensor); #endif -#ifdef USE_ESP32_CAMERA - void set_camera_state(std::shared_ptr image); - void send_camera_info(esp32_camera::ESP32Camera *camera); +#ifdef USE_CAMERA + void set_camera_state(std::shared_ptr image); void camera_image(const CameraImageRequest &msg) override; #endif #ifdef USE_CLIMATE bool send_climate_state(climate::Climate *climate); - void send_climate_info(climate::Climate *climate); void climate_command(const ClimateCommandRequest &msg) override; #endif #ifdef USE_NUMBER bool send_number_state(number::Number *number); - void send_number_info(number::Number *number); void number_command(const NumberCommandRequest &msg) override; #endif #ifdef USE_DATETIME_DATE bool send_date_state(datetime::DateEntity *date); - void send_date_info(datetime::DateEntity *date); void date_command(const DateCommandRequest &msg) override; #endif #ifdef USE_DATETIME_TIME bool send_time_state(datetime::TimeEntity *time); - void send_time_info(datetime::TimeEntity *time); void time_command(const TimeCommandRequest &msg) override; #endif #ifdef USE_DATETIME_DATETIME bool send_datetime_state(datetime::DateTimeEntity *datetime); - void send_datetime_info(datetime::DateTimeEntity *datetime); void datetime_command(const DateTimeCommandRequest &msg) override; #endif #ifdef USE_TEXT bool send_text_state(text::Text *text); - void send_text_info(text::Text *text); void text_command(const TextCommandRequest &msg) override; #endif #ifdef USE_SELECT bool send_select_state(select::Select *select); - void send_select_info(select::Select *select); void select_command(const SelectCommandRequest &msg) override; #endif #ifdef USE_BUTTON - void send_button_info(button::Button *button); void button_command(const ButtonCommandRequest &msg) override; #endif #ifdef USE_LOCK bool send_lock_state(lock::Lock *a_lock); - void send_lock_info(lock::Lock *a_lock); void lock_command(const LockCommandRequest &msg) override; #endif #ifdef USE_VALVE bool send_valve_state(valve::Valve *valve); - void send_valve_info(valve::Valve *valve); void valve_command(const ValveCommandRequest &msg) override; #endif #ifdef USE_MEDIA_PLAYER bool send_media_player_state(media_player::MediaPlayer *media_player); - void send_media_player_info(media_player::MediaPlayer *media_player); void media_player_command(const MediaPlayerCommandRequest &msg) override; #endif - bool try_send_log_message(int level, const char *tag, const char *line); + bool try_send_log_message(int level, const char *tag, const char *line, size_t message_len); void send_homeassistant_service_call(const HomeassistantServiceResponse &call) { - if (!this->service_call_subscription_) + if (!this->flags_.service_call_subscription) return; this->send_message(call); } @@ -167,26 +151,22 @@ class APIConnection : public APIServerConnection { #ifdef USE_ALARM_CONTROL_PANEL bool send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel); - void send_alarm_control_panel_info(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel); void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) override; #endif #ifdef USE_EVENT void send_event(event::Event *event, const std::string &event_type); - void send_event_info(event::Event *event); #endif #ifdef USE_UPDATE bool send_update_state(update::UpdateEntity *update); - void send_update_info(update::UpdateEntity *update); void update_command(const UpdateCommandRequest &msg) override; #endif void on_disconnect_response(const DisconnectResponse &value) override; void on_ping_response(const PingResponse &value) override { // we initiated ping - this->ping_retries_ = 0; - this->sent_ping_ = false; + this->flags_.sent_ping = false; } void on_home_assistant_state_response(const HomeAssistantStateResponse &msg) override; #ifdef USE_HOMEASSISTANT_TIME @@ -199,16 +179,16 @@ class APIConnection : public APIServerConnection { DeviceInfoResponse device_info(const DeviceInfoRequest &msg) override; void list_entities(const ListEntitiesRequest &msg) override { this->list_entities_iterator_.begin(); } void subscribe_states(const SubscribeStatesRequest &msg) override { - this->state_subscription_ = true; + this->flags_.state_subscription = true; this->initial_state_iterator_.begin(); } void subscribe_logs(const SubscribeLogsRequest &msg) override { - this->log_subscription_ = msg.level; + this->flags_.log_subscription = msg.level; if (msg.dump_config) App.schedule_dump_config(); } void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) override { - this->service_call_subscription_ = true; + this->flags_.service_call_subscription = true; } void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) override; GetTimeResponse get_time(const GetTimeRequest &msg) override { @@ -220,9 +200,12 @@ class APIConnection : public APIServerConnection { NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) override; #endif - bool is_authenticated() override { return this->connection_state_ == ConnectionState::AUTHENTICATED; } + bool is_authenticated() override { + return static_cast(this->flags_.connection_state) == ConnectionState::AUTHENTICATED; + } bool is_connection_setup() override { - return this->connection_state_ == ConnectionState ::CONNECTED || this->is_authenticated(); + return static_cast(this->flags_.connection_state) == ConnectionState::CONNECTED || + this->is_authenticated(); } void on_fatal_error() override; void on_unauthenticated_access() override; @@ -275,7 +258,13 @@ class APIConnection : public APIServerConnection { bool try_to_clear_buffer(bool log_out_of_space); bool send_buffer(ProtoWriteBuffer buffer, uint16_t message_type) override; - std::string get_client_combined_info() const { return this->client_combined_info_; } + std::string get_client_combined_info() const { + if (this->client_info_ == this->client_peername_) { + // Before Hello message, both are the same (just IP:port) + return this->client_info_; + } + return this->client_info_ + " (" + this->client_peername_ + ")"; + } // Buffer allocator methods for batch processing ProtoWriteBuffer allocate_single_message_buffer(uint16_t size); @@ -295,17 +284,42 @@ class APIConnection : public APIServerConnection { response.icon = entity->get_icon(); response.disabled_by_default = entity->is_disabled_by_default(); response.entity_category = static_cast(entity->get_entity_category()); +#ifdef USE_DEVICES + response.device_id = entity->get_device_id(); +#endif } // Helper function to fill common entity state fields static void fill_entity_state_base(esphome::EntityBase *entity, StateResponseProtoMessage &response) { response.key = entity->get_object_id_hash(); +#ifdef USE_DEVICES + response.device_id = entity->get_device_id(); +#endif } // Non-template helper to encode any ProtoMessage static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint16_t message_type, APIConnection *conn, uint32_t remaining_size, bool is_single); +#ifdef USE_VOICE_ASSISTANT + // Helper to check voice assistant validity and connection ownership + inline bool check_voice_assistant_api_connection_() const; +#endif + + // Helper method to process multiple entities from an iterator in a batch + template void process_iterator_batch_(Iterator &iterator) { + size_t initial_size = this->deferred_batch_.size(); + while (!iterator.completed() && (this->deferred_batch_.size() - initial_size) < MAX_INITIAL_PER_BATCH) { + iterator.advance(); + } + + // If the batch is full, process it immediately + // Note: iterator.advance() already calls schedule_batch_() via schedule_message_() + if (this->deferred_batch_.size() >= MAX_INITIAL_PER_BATCH) { + this->process_batch_(); + } + } + #ifdef USE_BINARY_SENSOR static uint16_t try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); @@ -416,7 +430,7 @@ class APIConnection : public APIServerConnection { static uint16_t try_send_update_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); #endif -#ifdef USE_ESP32_CAMERA +#ifdef USE_CAMERA static uint16_t try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); #endif @@ -432,124 +446,82 @@ class APIConnection : public APIServerConnection { // Helper function to get estimated message size for buffer pre-allocation static uint16_t get_estimated_message_size(uint16_t message_type); - enum class ConnectionState { - WAITING_FOR_HELLO, - CONNECTED, - AUTHENTICATED, - } connection_state_{ConnectionState::WAITING_FOR_HELLO}; + // Batch message method for ping requests + static uint16_t try_send_ping_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, + bool is_single); - bool remove_{false}; + // === Optimal member ordering for 32-bit systems === + // Group 1: Pointers (4 bytes each on 32-bit) std::unique_ptr helper_; - - std::string client_info_; - std::string client_peername_; - std::string client_combined_info_; - uint32_t client_api_version_major_{0}; - uint32_t client_api_version_minor_{0}; -#ifdef USE_ESP32_CAMERA - esp32_camera::CameraImageReader image_reader_; -#endif - - bool state_subscription_{false}; - int log_subscription_{ESPHOME_LOG_LEVEL_NONE}; - uint32_t last_traffic_; - uint32_t next_ping_retry_{0}; - uint8_t ping_retries_{0}; - bool sent_ping_{false}; - bool service_call_subscription_{false}; - bool next_close_ = false; APIServer *parent_; + + // Group 2: Larger objects (must be 4-byte aligned) + // These contain vectors/pointers internally, so putting them early ensures good alignment InitialStateIterator initial_state_iterator_; ListEntitiesIterator list_entities_iterator_; +#ifdef USE_CAMERA + std::unique_ptr image_reader_; +#endif + + // Group 3: Strings (12 bytes each on 32-bit, 4-byte aligned) + std::string client_info_; + std::string client_peername_; + + // Group 4: 4-byte types + uint32_t last_traffic_; int state_subs_at_ = -1; // Function pointer type for message encoding using MessageCreatorPtr = uint16_t (*)(EntityBase *, APIConnection *, uint32_t remaining_size, bool is_single); - // Optimized MessageCreator class using union dispatch class MessageCreator { public: - // Constructor for function pointer (message_type = 0) - MessageCreator(MessageCreatorPtr ptr) : message_type_(0) { data_.ptr = ptr; } + // Constructor for function pointer + MessageCreator(MessageCreatorPtr ptr) { data_.function_ptr = ptr; } // Constructor for string state capture - MessageCreator(const std::string &value, uint16_t msg_type) : message_type_(msg_type) { - data_.string_ptr = new std::string(value); - } + explicit MessageCreator(const std::string &str_value) { data_.string_ptr = new std::string(str_value); } - // Destructor - ~MessageCreator() { - // Clean up string data for string-based message types - if (uses_string_data_()) { - delete data_.string_ptr; - } - } + // No destructor - cleanup must be called explicitly with message_type - // Copy constructor - MessageCreator(const MessageCreator &other) : message_type_(other.message_type_) { - if (message_type_ == 0) { - data_.ptr = other.data_.ptr; - } else if (uses_string_data_()) { - data_.string_ptr = new std::string(*other.data_.string_ptr); - } else { - data_ = other.data_; // For POD types - } - } + // Delete copy operations - MessageCreator should only be moved + MessageCreator(const MessageCreator &other) = delete; + MessageCreator &operator=(const MessageCreator &other) = delete; // Move constructor - MessageCreator(MessageCreator &&other) noexcept : data_(other.data_), message_type_(other.message_type_) { - other.message_type_ = 0; // Reset other to function pointer type - other.data_.ptr = nullptr; - } - - // Assignment operators (needed for batch deduplication) - MessageCreator &operator=(const MessageCreator &other) { - if (this != &other) { - // Clean up current string data if needed - if (uses_string_data_()) { - delete data_.string_ptr; - } - // Copy new data - message_type_ = other.message_type_; - if (other.message_type_ == 0) { - data_.ptr = other.data_.ptr; - } else if (other.uses_string_data_()) { - data_.string_ptr = new std::string(*other.data_.string_ptr); - } else { - data_ = other.data_; - } - } - return *this; - } + MessageCreator(MessageCreator &&other) noexcept : data_(other.data_) { other.data_.function_ptr = nullptr; } + // Move assignment MessageCreator &operator=(MessageCreator &&other) noexcept { if (this != &other) { - // Clean up current string data if needed - if (uses_string_data_()) { - delete data_.string_ptr; - } - // Move data - message_type_ = other.message_type_; + // IMPORTANT: Caller must ensure cleanup() was called if this contains a string! + // In our usage, this happens in add_item() deduplication and vector::erase() data_ = other.data_; - // Reset other to safe state - other.message_type_ = 0; - other.data_.ptr = nullptr; + other.data_.function_ptr = nullptr; } return *this; } - // Call operator - uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) const; + // 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 message_type) const; + + // Manual cleanup method - must be called before destruction for string types + void cleanup(uint16_t message_type) { +#ifdef USE_EVENT + if (message_type == EventResponse::MESSAGE_TYPE && data_.string_ptr != nullptr) { + delete data_.string_ptr; + data_.string_ptr = nullptr; + } +#endif + } private: - // Helper to check if this message type uses heap-allocated strings - bool uses_string_data_() const { return message_type_ == EventResponse::MESSAGE_TYPE; } - union CreatorData { - MessageCreatorPtr ptr; // 8 bytes - std::string *string_ptr; // 8 bytes - } data_; // 8 bytes - uint16_t message_type_; // 2 bytes (0 = function ptr, >0 = state capture) + union Data { + MessageCreatorPtr function_ptr; + std::string *string_ptr; + } data_; // 4 bytes on 32-bit, 8 bytes on 64-bit - same as before }; // Generic batching mechanism for both state updates and entity info @@ -566,24 +538,86 @@ class APIConnection : public APIServerConnection { std::vector items; uint32_t batch_start_time{0}; - bool batch_scheduled{false}; + private: + // Helper to cleanup items from the beginning + void cleanup_items_(size_t count) { + for (size_t i = 0; i < count; i++) { + items[i].creator.cleanup(items[i].message_type); + } + } + + public: DeferredBatch() { // Pre-allocate capacity for typical batch sizes to avoid reallocation items.reserve(8); } + ~DeferredBatch() { + // Ensure cleanup of any remaining items + clear(); + } + // Add item to the batch void add_item(EntityBase *entity, MessageCreator creator, uint16_t message_type); + // 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); + + // Clear all items with proper cleanup void clear() { + cleanup_items_(items.size()); items.clear(); - batch_scheduled = false; batch_start_time = 0; } + + // Remove processed items from the front with proper cleanup + void remove_front(size_t count) { + cleanup_items_(count); + items.erase(items.begin(), items.begin() + count); + } + bool empty() const { return items.empty(); } + size_t size() const { return items.size(); } + const BatchItem &operator[](size_t index) const { return items[index]; } }; + // DeferredBatch here (16 bytes, 4-byte aligned) DeferredBatch deferred_batch_; + + // ConnectionState enum for type safety + enum class ConnectionState : uint8_t { + WAITING_FOR_HELLO = 0, + CONNECTED = 1, + AUTHENTICATED = 2, + }; + + // Group 5: Pack all small members together to minimize padding + // This group starts at a 4-byte boundary after DeferredBatch + struct APIFlags { + // Connection state only needs 2 bits (3 states) + uint8_t connection_state : 2; + // Log subscription needs 3 bits (log levels 0-7) + uint8_t log_subscription : 3; + // Boolean flags (1 bit each) + uint8_t remove : 1; + uint8_t state_subscription : 1; + uint8_t sent_ping : 1; + + uint8_t service_call_subscription : 1; + uint8_t next_close : 1; + uint8_t batch_scheduled : 1; + uint8_t batch_first_message : 1; // For batch buffer allocation + uint8_t should_try_send_immediately : 1; // True after initial states are sent +#ifdef HAS_PROTO_MESSAGE_DUMP + uint8_t log_only_mode : 1; +#endif + } flags_{}; // 2 bytes total + + // 2-byte types immediately after flags_ (no padding between them) + uint16_t client_api_version_major_{0}; + uint16_t client_api_version_minor_{0}; + // Total: 2 (flags) + 2 + 2 = 6 bytes, then 2 bytes padding to next 4-byte boundary + uint32_t get_batch_delay_ms_() const; // Message will use 8 more bytes than the minimum size, and typical // MTU is 1500. Sometimes users will see as low as 1460 MTU. @@ -600,9 +634,49 @@ class APIConnection : public APIServerConnection { bool schedule_batch_(); void process_batch_(); + void clear_batch_() { + this->deferred_batch_.clear(); + this->flags_.batch_scheduled = false; + } - // State for batch buffer allocation - bool batch_first_message_{false}; +#ifdef HAS_PROTO_MESSAGE_DUMP + // Helper to log a proto message from a MessageCreator object + void log_proto_message_(EntityBase *entity, const MessageCreator &creator, uint16_t message_type) { + this->flags_.log_only_mode = true; + creator(entity, this, MAX_PACKET_SIZE, true, message_type); + this->flags_.log_only_mode = false; + } + + void log_batch_item_(const DeferredBatch::BatchItem &item) { + // Use the helper to log the message + this->log_proto_message_(item.entity, item.creator, item.message_type); + } +#endif + + // Helper method to send a message either immediately or via batching + bool send_message_smart_(EntityBase *entity, MessageCreatorPtr creator, uint16_t message_type) { + // Try to send immediately if: + // 1. We should try to send immediately (should_try_send_immediately = true) + // 2. Batch delay is 0 (user has opted in to immediate sending) + // 3. Buffer has space available + if (this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0 && + this->helper_->can_write_without_blocking()) { + // Now actually encode and send + if (creator(entity, this, MAX_PACKET_SIZE, true) && + this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) { +#ifdef HAS_PROTO_MESSAGE_DUMP + // Log the message in verbose mode + this->log_proto_message_(entity, MessageCreator(creator), message_type); +#endif + return true; + } + + // If immediate send failed, fall through to batching + } + + // Fall back to scheduled batching + return this->schedule_message_(entity, creator, message_type); + } // Helper function to schedule a deferred message with known message type bool schedule_message_(EntityBase *entity, MessageCreator creator, uint16_t message_type) { @@ -614,6 +688,12 @@ class APIConnection : public APIServerConnection { bool schedule_message_(EntityBase *entity, MessageCreatorPtr function_ptr, uint16_t message_type) { return schedule_message_(entity, MessageCreator(function_ptr), message_type); } + + // 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) { + this->deferred_batch_.add_item_front(entity, MessageCreator(function_ptr), message_type); + return this->schedule_batch_(); + } }; } // namespace api diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index e0eb94836d..2f5acc3bfa 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -66,6 +66,17 @@ const char *api_error_to_str(APIError err) { return "UNKNOWN"; } +// Default implementation for loop - handles sending buffered data +APIError APIFrameHelper::loop() { + if (!this->tx_buf_.empty()) { + APIError err = try_send_tx_buf_(); + if (err != APIError::OK && err != APIError::WOULD_BLOCK) { + return err; + } + } + return APIError::OK; // Convert WOULD_BLOCK to OK to avoid connection termination +} + // Helper method to buffer data from IOVs void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len) { SendBuffer buffer; @@ -214,6 +225,22 @@ APIError APIFrameHelper::init_common_() { } #define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->info_.c_str(), ##__VA_ARGS__) + +APIError APIFrameHelper::handle_socket_read_result_(ssize_t received) { + if (received == -1) { + if (errno == EWOULDBLOCK || errno == EAGAIN) { + return APIError::WOULD_BLOCK; + } + state_ = State::FAILED; + HELPER_LOG("Socket read failed with errno %d", errno); + return APIError::SOCKET_READ_FAILED; + } else if (received == 0) { + state_ = State::FAILED; + HELPER_LOG("Connection closed"); + return APIError::CONNECTION_CLOSED; + } + return APIError::OK; +} // uncomment to log raw packets //#define HELPER_LOG_PACKETS @@ -274,17 +301,21 @@ APIError APINoiseFrameHelper::init() { } /// Run through handshake messages (if in that phase) APIError APINoiseFrameHelper::loop() { - APIError err = state_action_(); - if (err != APIError::OK && err != APIError::WOULD_BLOCK) { - return err; - } - if (!this->tx_buf_.empty()) { - err = try_send_tx_buf_(); + // During handshake phase, process as many actions as possible until we can't progress + // socket_->ready() stays true until next main loop, but state_action() will return + // WOULD_BLOCK when no more data is available to read + while (state_ != State::DATA && this->socket_->ready()) { + APIError err = state_action_(); if (err != APIError::OK && err != APIError::WOULD_BLOCK) { return err; } + if (err == APIError::WOULD_BLOCK) { + break; + } } - return APIError::OK; // Convert WOULD_BLOCK to OK to avoid connection termination + + // Use base class implementation for buffer sending + return APIFrameHelper::loop(); } /** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter @@ -312,17 +343,9 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) { // no header information yet uint8_t to_read = 3 - rx_header_buf_len_; ssize_t received = this->socket_->read(&rx_header_buf_[rx_header_buf_len_], to_read); - if (received == -1) { - if (errno == EWOULDBLOCK || errno == EAGAIN) { - return APIError::WOULD_BLOCK; - } - state_ = State::FAILED; - HELPER_LOG("Socket read failed with errno %d", errno); - return APIError::SOCKET_READ_FAILED; - } else if (received == 0) { - state_ = State::FAILED; - HELPER_LOG("Connection closed"); - return APIError::CONNECTION_CLOSED; + APIError err = handle_socket_read_result_(received); + if (err != APIError::OK) { + return err; } rx_header_buf_len_ += static_cast(received); if (static_cast(received) != to_read) { @@ -330,17 +353,15 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) { return APIError::WOULD_BLOCK; } + if (rx_header_buf_[0] != 0x01) { + state_ = State::FAILED; + HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]); + return APIError::BAD_INDICATOR; + } // header reading done } // read body - uint8_t indicator = rx_header_buf_[0]; - if (indicator != 0x01) { - state_ = State::FAILED; - HELPER_LOG("Bad indicator byte %u", indicator); - return APIError::BAD_INDICATOR; - } - uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2]; if (state_ != State::DATA && msg_size > 128) { @@ -359,17 +380,9 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) { // more data to read uint16_t to_read = msg_size - rx_buf_len_; ssize_t received = this->socket_->read(&rx_buf_[rx_buf_len_], to_read); - if (received == -1) { - if (errno == EWOULDBLOCK || errno == EAGAIN) { - return APIError::WOULD_BLOCK; - } - state_ = State::FAILED; - HELPER_LOG("Socket read failed with errno %d", errno); - return APIError::SOCKET_READ_FAILED; - } else if (received == 0) { - state_ = State::FAILED; - HELPER_LOG("Connection closed"); - return APIError::CONNECTION_CLOSED; + APIError err = handle_socket_read_result_(received); + if (err != APIError::OK) { + return err; } rx_buf_len_ += static_cast(received); if (static_cast(received) != to_read) { @@ -586,10 +599,6 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { return APIError::BAD_DATA_PACKET; } - // uint16_t type; - // uint16_t data_len; - // uint8_t *data; - // uint8_t *padding; zero or more bytes to fill up the rest of the packet uint16_t type = (((uint16_t) msg_data[0]) << 8) | msg_data[1]; uint16_t data_len = (((uint16_t) msg_data[2]) << 8) | msg_data[3]; if (data_len > msg_size - 4) { @@ -605,20 +614,14 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { return APIError::OK; } APIError APINoiseFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) { - std::vector *raw_buffer = buffer.get_buffer(); - uint16_t payload_len = static_cast(raw_buffer->size() - frame_header_padding_); - // Resize to include MAC space (required for Noise encryption) - raw_buffer->resize(raw_buffer->size() + frame_footer_size_); - - // Use write_protobuf_packets with a single packet - std::vector packets; - packets.emplace_back(type, 0, payload_len); - - return write_protobuf_packets(buffer, packets); + buffer.get_buffer()->resize(buffer.get_buffer()->size() + frame_footer_size_); + PacketInfo packet{type, 0, + static_cast(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)}; + return write_protobuf_packets(buffer, std::span(&packet, 1)); } -APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, const std::vector &packets) { +APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, std::span packets) { APIError aerr = state_action_(); if (aerr != APIError::OK) { return aerr; @@ -633,18 +636,15 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, co } std::vector *raw_buffer = buffer.get_buffer(); + uint8_t *buffer_data = raw_buffer->data(); // Cache buffer pointer + this->reusable_iovs_.clear(); this->reusable_iovs_.reserve(packets.size()); // We need to encrypt each packet in place for (const auto &packet : packets) { - uint16_t type = packet.message_type; - uint16_t offset = packet.offset; - uint16_t payload_len = packet.payload_size; - uint16_t msg_len = 4 + payload_len; // type(2) + data_len(2) + payload - // The buffer already has padding at offset - uint8_t *buf_start = raw_buffer->data() + offset; + uint8_t *buf_start = buffer_data + packet.offset; // Write noise header buf_start[0] = 0x01; // indicator @@ -652,10 +652,10 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, co // Write message header (to be encrypted) const uint8_t msg_offset = 3; - buf_start[msg_offset + 0] = (uint8_t) (type >> 8); // type high byte - buf_start[msg_offset + 1] = (uint8_t) type; // type low byte - buf_start[msg_offset + 2] = (uint8_t) (payload_len >> 8); // data_len high byte - buf_start[msg_offset + 3] = (uint8_t) payload_len; // data_len low byte + buf_start[msg_offset] = static_cast(packet.message_type >> 8); // type high byte + buf_start[msg_offset + 1] = static_cast(packet.message_type); // type low byte + buf_start[msg_offset + 2] = static_cast(packet.payload_size >> 8); // data_len high byte + buf_start[msg_offset + 3] = static_cast(packet.payload_size); // data_len low byte // payload data is already in the buffer starting at offset + 7 // Make sure we have space for MAC @@ -664,7 +664,8 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, co // Encrypt the message in place NoiseBuffer mbuf; noise_buffer_init(mbuf); - noise_buffer_set_inout(mbuf, buf_start + msg_offset, msg_len, msg_len + frame_footer_size_); + noise_buffer_set_inout(mbuf, buf_start + msg_offset, 4 + packet.payload_size, + 4 + packet.payload_size + frame_footer_size_); int err = noise_cipherstate_encrypt(send_cipher_, &mbuf); if (err != 0) { @@ -674,14 +675,12 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, co } // Fill in the encrypted size - buf_start[1] = (uint8_t) (mbuf.size >> 8); - buf_start[2] = (uint8_t) mbuf.size; + buf_start[1] = static_cast(mbuf.size >> 8); + buf_start[2] = static_cast(mbuf.size); // Add iovec for this encrypted packet - struct iovec iov; - iov.iov_base = buf_start; - iov.iov_len = 3 + mbuf.size; // indicator + size + encrypted data - this->reusable_iovs_.push_back(iov); + this->reusable_iovs_.push_back( + {buf_start, static_cast(3 + mbuf.size)}); // indicator + size + encrypted data } // Send all encrypted packets in one writev call @@ -822,18 +821,12 @@ APIError APIPlaintextFrameHelper::init() { state_ = State::DATA; return APIError::OK; } -/// Not used for plaintext APIError APIPlaintextFrameHelper::loop() { if (state_ != State::DATA) { return APIError::BAD_STATE; } - if (!this->tx_buf_.empty()) { - APIError err = try_send_tx_buf_(); - if (err != APIError::OK && err != APIError::WOULD_BLOCK) { - return err; - } - } - return APIError::OK; // Convert WOULD_BLOCK to OK to avoid connection termination + // Use base class implementation for buffer sending + return APIFrameHelper::loop(); } /** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter @@ -862,17 +855,9 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) { // Try to get to at least 3 bytes total (indicator + 2 varint bytes), then read one byte at a time ssize_t received = this->socket_->read(&rx_header_buf_[rx_header_buf_pos_], rx_header_buf_pos_ < 3 ? 3 - rx_header_buf_pos_ : 1); - if (received == -1) { - if (errno == EWOULDBLOCK || errno == EAGAIN) { - return APIError::WOULD_BLOCK; - } - state_ = State::FAILED; - HELPER_LOG("Socket read failed with errno %d", errno); - return APIError::SOCKET_READ_FAILED; - } else if (received == 0) { - state_ = State::FAILED; - HELPER_LOG("Connection closed"); - return APIError::CONNECTION_CLOSED; + APIError err = handle_socket_read_result_(received); + if (err != APIError::OK) { + return err; } // If this was the first read, validate the indicator byte @@ -956,17 +941,9 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) { // more data to read uint16_t to_read = rx_header_parsed_len_ - rx_buf_len_; ssize_t received = this->socket_->read(&rx_buf_[rx_buf_len_], to_read); - if (received == -1) { - if (errno == EWOULDBLOCK || errno == EAGAIN) { - return APIError::WOULD_BLOCK; - } - state_ = State::FAILED; - HELPER_LOG("Socket read failed with errno %d", errno); - return APIError::SOCKET_READ_FAILED; - } else if (received == 0) { - state_ = State::FAILED; - HELPER_LOG("Connection closed"); - return APIError::CONNECTION_CLOSED; + APIError err = handle_socket_read_result_(received); + if (err != APIError::OK) { + return err; } rx_buf_len_ += static_cast(received); if (static_cast(received) != to_read) { @@ -1026,18 +1003,11 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { return APIError::OK; } APIError APIPlaintextFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) { - std::vector *raw_buffer = buffer.get_buffer(); - uint16_t payload_len = static_cast(raw_buffer->size() - frame_header_padding_); - - // Use write_protobuf_packets with a single packet - std::vector packets; - packets.emplace_back(type, 0, payload_len); - - return write_protobuf_packets(buffer, packets); + PacketInfo packet{type, 0, static_cast(buffer.get_buffer()->size() - frame_header_padding_)}; + return write_protobuf_packets(buffer, std::span(&packet, 1)); } -APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, - const std::vector &packets) { +APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, std::span packets) { if (state_ != State::DATA) { return APIError::BAD_STATE; } @@ -1047,17 +1017,15 @@ APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer } std::vector *raw_buffer = buffer.get_buffer(); + uint8_t *buffer_data = raw_buffer->data(); // Cache buffer pointer + this->reusable_iovs_.clear(); this->reusable_iovs_.reserve(packets.size()); for (const auto &packet : packets) { - uint16_t type = packet.message_type; - uint16_t offset = packet.offset; - uint16_t payload_len = packet.payload_size; - // Calculate varint sizes for header layout - uint8_t size_varint_len = api::ProtoSize::varint(static_cast(payload_len)); - uint8_t type_varint_len = api::ProtoSize::varint(static_cast(type)); + uint8_t size_varint_len = api::ProtoSize::varint(static_cast(packet.payload_size)); + uint8_t type_varint_len = api::ProtoSize::varint(static_cast(packet.message_type)); uint8_t total_header_len = 1 + size_varint_len + type_varint_len; // Calculate where to start writing the header @@ -1085,23 +1053,20 @@ APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer // // The message starts at offset + frame_header_padding_ // So we write the header starting at offset + frame_header_padding_ - total_header_len - uint8_t *buf_start = raw_buffer->data() + offset; + uint8_t *buf_start = buffer_data + packet.offset; uint32_t header_offset = frame_header_padding_ - total_header_len; // Write the plaintext header buf_start[header_offset] = 0x00; // indicator - // Encode size varint directly into buffer - ProtoVarInt(payload_len).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_len); - - // Encode type varint directly into buffer - ProtoVarInt(type).encode_to_buffer_unchecked(buf_start + header_offset + 1 + size_varint_len, type_varint_len); + // Encode varints directly into buffer + ProtoVarInt(packet.payload_size).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_len); + ProtoVarInt(packet.message_type) + .encode_to_buffer_unchecked(buf_start + header_offset + 1 + size_varint_len, type_varint_len); // Add iovec for this packet (header + payload) - struct iovec iov; - iov.iov_base = buf_start + header_offset; - iov.iov_len = total_header_len + payload_len; - this->reusable_iovs_.push_back(iov); + this->reusable_iovs_.push_back( + {buf_start + header_offset, static_cast(total_header_len + packet.payload_size)}); } // Send all packets in one writev call diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index dc71a7ca17..eae83a3484 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -2,6 +2,7 @@ #include #include #include +#include #include #include @@ -38,7 +39,7 @@ struct PacketInfo { : message_type(type), offset(off), payload_size(size), padding(0) {} }; -enum class APIError : int { +enum class APIError : uint16_t { OK = 0, WOULD_BLOCK = 1001, BAD_HANDSHAKE_PACKET_LEN = 1002, @@ -74,7 +75,7 @@ class APIFrameHelper { } virtual ~APIFrameHelper() = default; virtual APIError init() = 0; - virtual APIError loop() = 0; + virtual APIError loop(); virtual APIError read_packet(ReadPacketBuffer *buffer) = 0; bool can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); } std::string getpeername() { return socket_->getpeername(); } @@ -101,7 +102,7 @@ class APIFrameHelper { // Write multiple protobuf packets in a single operation // packets contains (message_type, offset, length) for each message in the buffer // The buffer contains all messages with appropriate padding before each - virtual APIError write_protobuf_packets(ProtoWriteBuffer buffer, const std::vector &packets) = 0; + virtual APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span packets) = 0; // Get the frame header padding required by this protocol virtual uint8_t frame_header_padding() = 0; // Get the frame footer size required by this protocol @@ -125,38 +126,6 @@ class APIFrameHelper { const uint8_t *current_data() const { return data.data() + offset; } }; - // Queue of data buffers to be sent - std::deque tx_buf_; - - // Common state enum for all frame helpers - // Note: Not all states are used by all implementations - // - INITIALIZE: Used by both Noise and Plaintext - // - CLIENT_HELLO, SERVER_HELLO, HANDSHAKE: Only used by Noise protocol - // - DATA: Used by both Noise and Plaintext - // - CLOSED: Used by both Noise and Plaintext - // - FAILED: Used by both Noise and Plaintext - // - EXPLICIT_REJECT: Only used by Noise protocol - enum class State { - INITIALIZE = 1, - CLIENT_HELLO = 2, // Noise only - SERVER_HELLO = 3, // Noise only - HANDSHAKE = 4, // Noise only - DATA = 5, - CLOSED = 6, - FAILED = 7, - EXPLICIT_REJECT = 8, // Noise only - }; - - // Current state of the frame helper - State state_{State::INITIALIZE}; - - // Helper name for logging - std::string info_; - - // Socket for communication - socket::Socket *socket_{nullptr}; - std::unique_ptr socket_owned_; - // Common implementation for writing raw data to socket APIError write_raw_(const struct iovec *iov, int iovcnt); @@ -169,18 +138,47 @@ class APIFrameHelper { APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector &tx_buf, const std::string &info, StateEnum &state, StateEnum failed_state); + // Pointers first (4 bytes each) + socket::Socket *socket_{nullptr}; + std::unique_ptr socket_owned_; + + // Common state enum for all frame helpers + // Note: Not all states are used by all implementations + // - INITIALIZE: Used by both Noise and Plaintext + // - CLIENT_HELLO, SERVER_HELLO, HANDSHAKE: Only used by Noise protocol + // - DATA: Used by both Noise and Plaintext + // - CLOSED: Used by both Noise and Plaintext + // - FAILED: Used by both Noise and Plaintext + // - EXPLICIT_REJECT: Only used by Noise protocol + enum class State : uint8_t { + INITIALIZE = 1, + CLIENT_HELLO = 2, // Noise only + SERVER_HELLO = 3, // Noise only + HANDSHAKE = 4, // Noise only + DATA = 5, + CLOSED = 6, + FAILED = 7, + EXPLICIT_REJECT = 8, // Noise only + }; + + // Containers (size varies, but typically 12+ bytes on 32-bit) + std::deque tx_buf_; + std::string info_; + std::vector reusable_iovs_; + std::vector rx_buf_; + + // Group smaller types together + uint16_t rx_buf_len_ = 0; + State state_{State::INITIALIZE}; uint8_t frame_header_padding_{0}; uint8_t frame_footer_size_{0}; - - // Reusable IOV array for write_protobuf_packets to avoid repeated allocations - std::vector reusable_iovs_; - - // Receive buffer for reading frame data - std::vector rx_buf_; - uint16_t rx_buf_len_ = 0; + // 5 bytes total, 3 bytes padding // Common initialization for both plaintext and noise protocols APIError init_common_(); + + // Helper method to handle socket read results + APIError handle_socket_read_result_(ssize_t received); }; #ifdef USE_API_NOISE @@ -200,7 +198,7 @@ class APINoiseFrameHelper : public APIFrameHelper { APIError loop() override; APIError read_packet(ReadPacketBuffer *buffer) override; APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) override; - APIError write_protobuf_packets(ProtoWriteBuffer buffer, const std::vector &packets) override; + APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span packets) override; // Get the frame header padding required by this protocol uint8_t frame_header_padding() override { return frame_header_padding_; } // Get the frame footer size required by this protocol @@ -213,19 +211,28 @@ class APINoiseFrameHelper : public APIFrameHelper { APIError init_handshake_(); APIError check_handshake_finished_(); void send_explicit_handshake_reject_(const std::string &reason); + + // Pointers first (4 bytes each) + NoiseHandshakeState *handshake_{nullptr}; + NoiseCipherState *send_cipher_{nullptr}; + NoiseCipherState *recv_cipher_{nullptr}; + + // Shared pointer (8 bytes on 32-bit = 4 bytes control block pointer + 4 bytes object pointer) + std::shared_ptr ctx_; + + // Vector (12 bytes on 32-bit) + std::vector prologue_; + + // NoiseProtocolId (size depends on implementation) + NoiseProtocolId nid_; + + // Group small types together // Fixed-size header buffer for noise protocol: // 1 byte for indicator + 2 bytes for message size (16-bit value, not varint) // Note: Maximum message size is UINT16_MAX (65535), with a limit of 128 bytes during handshake phase uint8_t rx_header_buf_[3]; uint8_t rx_header_buf_len_ = 0; - - std::vector prologue_; - - std::shared_ptr ctx_; - NoiseHandshakeState *handshake_{nullptr}; - NoiseCipherState *send_cipher_{nullptr}; - NoiseCipherState *recv_cipher_{nullptr}; - NoiseProtocolId nid_; + // 4 bytes total, no padding }; #endif // USE_API_NOISE @@ -245,13 +252,19 @@ class APIPlaintextFrameHelper : public APIFrameHelper { APIError loop() override; APIError read_packet(ReadPacketBuffer *buffer) override; APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) override; - APIError write_protobuf_packets(ProtoWriteBuffer buffer, const std::vector &packets) override; + APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span packets) override; uint8_t frame_header_padding() override { return frame_header_padding_; } // Get the frame footer size required by this protocol uint8_t frame_footer_size() override { return frame_footer_size_; } protected: APIError try_read_frame_(ParsedFrame *frame); + + // Group 2-byte aligned types + uint16_t rx_header_parsed_type_ = 0; + uint16_t rx_header_parsed_len_ = 0; + + // Group 1-byte types together // Fixed-size header buffer for plaintext protocol: // We now store the indicator byte + the two varints. // To match noise protocol's maximum message size (UINT16_MAX = 65535), we need: @@ -263,8 +276,7 @@ class APIPlaintextFrameHelper : public APIFrameHelper { uint8_t rx_header_buf_[6]; // 1 byte indicator + 5 bytes for varints (3 for size + 2 for type) uint8_t rx_header_buf_pos_ = 0; bool rx_header_parsed_ = false; - uint16_t rx_header_parsed_type_ = 0; - uint16_t rx_header_parsed_len_ = 0; + // 8 bytes total, no padding needed }; #endif diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index bde1824d71..3505ec758d 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -3,634 +3,11 @@ #include "api_pb2.h" #include "api_pb2_size.h" #include "esphome/core/log.h" - -#include +#include "esphome/core/helpers.h" namespace esphome { namespace api { -#ifdef HAS_PROTO_MESSAGE_DUMP -template<> const char *proto_enum_to_string(enums::EntityCategory value) { - switch (value) { - case enums::ENTITY_CATEGORY_NONE: - return "ENTITY_CATEGORY_NONE"; - case enums::ENTITY_CATEGORY_CONFIG: - return "ENTITY_CATEGORY_CONFIG"; - case enums::ENTITY_CATEGORY_DIAGNOSTIC: - return "ENTITY_CATEGORY_DIAGNOSTIC"; - default: - return "UNKNOWN"; - } -} -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP -template<> const char *proto_enum_to_string(enums::LegacyCoverState value) { - switch (value) { - case enums::LEGACY_COVER_STATE_OPEN: - return "LEGACY_COVER_STATE_OPEN"; - case enums::LEGACY_COVER_STATE_CLOSED: - return "LEGACY_COVER_STATE_CLOSED"; - default: - return "UNKNOWN"; - } -} -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP -template<> const char *proto_enum_to_string(enums::CoverOperation value) { - switch (value) { - case enums::COVER_OPERATION_IDLE: - return "COVER_OPERATION_IDLE"; - case enums::COVER_OPERATION_IS_OPENING: - return "COVER_OPERATION_IS_OPENING"; - case enums::COVER_OPERATION_IS_CLOSING: - return "COVER_OPERATION_IS_CLOSING"; - default: - return "UNKNOWN"; - } -} -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP -template<> const char *proto_enum_to_string(enums::LegacyCoverCommand value) { - switch (value) { - case enums::LEGACY_COVER_COMMAND_OPEN: - return "LEGACY_COVER_COMMAND_OPEN"; - case enums::LEGACY_COVER_COMMAND_CLOSE: - return "LEGACY_COVER_COMMAND_CLOSE"; - case enums::LEGACY_COVER_COMMAND_STOP: - return "LEGACY_COVER_COMMAND_STOP"; - default: - return "UNKNOWN"; - } -} -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP -template<> const char *proto_enum_to_string(enums::FanSpeed value) { - switch (value) { - case enums::FAN_SPEED_LOW: - return "FAN_SPEED_LOW"; - case enums::FAN_SPEED_MEDIUM: - return "FAN_SPEED_MEDIUM"; - case enums::FAN_SPEED_HIGH: - return "FAN_SPEED_HIGH"; - default: - return "UNKNOWN"; - } -} -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP -template<> const char *proto_enum_to_string(enums::FanDirection value) { - switch (value) { - case enums::FAN_DIRECTION_FORWARD: - return "FAN_DIRECTION_FORWARD"; - case enums::FAN_DIRECTION_REVERSE: - return "FAN_DIRECTION_REVERSE"; - default: - return "UNKNOWN"; - } -} -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP -template<> const char *proto_enum_to_string(enums::ColorMode value) { - switch (value) { - case enums::COLOR_MODE_UNKNOWN: - return "COLOR_MODE_UNKNOWN"; - case enums::COLOR_MODE_ON_OFF: - return "COLOR_MODE_ON_OFF"; - case enums::COLOR_MODE_LEGACY_BRIGHTNESS: - return "COLOR_MODE_LEGACY_BRIGHTNESS"; - case enums::COLOR_MODE_BRIGHTNESS: - return "COLOR_MODE_BRIGHTNESS"; - case enums::COLOR_MODE_WHITE: - return "COLOR_MODE_WHITE"; - case enums::COLOR_MODE_COLOR_TEMPERATURE: - return "COLOR_MODE_COLOR_TEMPERATURE"; - case enums::COLOR_MODE_COLD_WARM_WHITE: - return "COLOR_MODE_COLD_WARM_WHITE"; - case enums::COLOR_MODE_RGB: - return "COLOR_MODE_RGB"; - case enums::COLOR_MODE_RGB_WHITE: - return "COLOR_MODE_RGB_WHITE"; - case enums::COLOR_MODE_RGB_COLOR_TEMPERATURE: - return "COLOR_MODE_RGB_COLOR_TEMPERATURE"; - case enums::COLOR_MODE_RGB_COLD_WARM_WHITE: - return "COLOR_MODE_RGB_COLD_WARM_WHITE"; - default: - return "UNKNOWN"; - } -} -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP -template<> const char *proto_enum_to_string(enums::SensorStateClass value) { - switch (value) { - case enums::STATE_CLASS_NONE: - return "STATE_CLASS_NONE"; - case enums::STATE_CLASS_MEASUREMENT: - return "STATE_CLASS_MEASUREMENT"; - case enums::STATE_CLASS_TOTAL_INCREASING: - return "STATE_CLASS_TOTAL_INCREASING"; - case enums::STATE_CLASS_TOTAL: - return "STATE_CLASS_TOTAL"; - default: - return "UNKNOWN"; - } -} -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP -template<> const char *proto_enum_to_string(enums::SensorLastResetType value) { - switch (value) { - case enums::LAST_RESET_NONE: - return "LAST_RESET_NONE"; - case enums::LAST_RESET_NEVER: - return "LAST_RESET_NEVER"; - case enums::LAST_RESET_AUTO: - return "LAST_RESET_AUTO"; - default: - return "UNKNOWN"; - } -} -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP -template<> const char *proto_enum_to_string(enums::LogLevel value) { - switch (value) { - case enums::LOG_LEVEL_NONE: - return "LOG_LEVEL_NONE"; - case enums::LOG_LEVEL_ERROR: - return "LOG_LEVEL_ERROR"; - case enums::LOG_LEVEL_WARN: - return "LOG_LEVEL_WARN"; - case enums::LOG_LEVEL_INFO: - return "LOG_LEVEL_INFO"; - case enums::LOG_LEVEL_CONFIG: - return "LOG_LEVEL_CONFIG"; - case enums::LOG_LEVEL_DEBUG: - return "LOG_LEVEL_DEBUG"; - case enums::LOG_LEVEL_VERBOSE: - return "LOG_LEVEL_VERBOSE"; - case enums::LOG_LEVEL_VERY_VERBOSE: - return "LOG_LEVEL_VERY_VERBOSE"; - default: - return "UNKNOWN"; - } -} -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP -template<> const char *proto_enum_to_string(enums::ServiceArgType value) { - switch (value) { - case enums::SERVICE_ARG_TYPE_BOOL: - return "SERVICE_ARG_TYPE_BOOL"; - case enums::SERVICE_ARG_TYPE_INT: - return "SERVICE_ARG_TYPE_INT"; - case enums::SERVICE_ARG_TYPE_FLOAT: - return "SERVICE_ARG_TYPE_FLOAT"; - case enums::SERVICE_ARG_TYPE_STRING: - return "SERVICE_ARG_TYPE_STRING"; - case enums::SERVICE_ARG_TYPE_BOOL_ARRAY: - return "SERVICE_ARG_TYPE_BOOL_ARRAY"; - case enums::SERVICE_ARG_TYPE_INT_ARRAY: - return "SERVICE_ARG_TYPE_INT_ARRAY"; - case enums::SERVICE_ARG_TYPE_FLOAT_ARRAY: - return "SERVICE_ARG_TYPE_FLOAT_ARRAY"; - case enums::SERVICE_ARG_TYPE_STRING_ARRAY: - return "SERVICE_ARG_TYPE_STRING_ARRAY"; - default: - return "UNKNOWN"; - } -} -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP -template<> const char *proto_enum_to_string(enums::ClimateMode value) { - switch (value) { - case enums::CLIMATE_MODE_OFF: - return "CLIMATE_MODE_OFF"; - case enums::CLIMATE_MODE_HEAT_COOL: - return "CLIMATE_MODE_HEAT_COOL"; - case enums::CLIMATE_MODE_COOL: - return "CLIMATE_MODE_COOL"; - case enums::CLIMATE_MODE_HEAT: - return "CLIMATE_MODE_HEAT"; - case enums::CLIMATE_MODE_FAN_ONLY: - return "CLIMATE_MODE_FAN_ONLY"; - case enums::CLIMATE_MODE_DRY: - return "CLIMATE_MODE_DRY"; - case enums::CLIMATE_MODE_AUTO: - return "CLIMATE_MODE_AUTO"; - default: - return "UNKNOWN"; - } -} -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP -template<> const char *proto_enum_to_string(enums::ClimateFanMode value) { - switch (value) { - case enums::CLIMATE_FAN_ON: - return "CLIMATE_FAN_ON"; - case enums::CLIMATE_FAN_OFF: - return "CLIMATE_FAN_OFF"; - case enums::CLIMATE_FAN_AUTO: - return "CLIMATE_FAN_AUTO"; - case enums::CLIMATE_FAN_LOW: - return "CLIMATE_FAN_LOW"; - case enums::CLIMATE_FAN_MEDIUM: - return "CLIMATE_FAN_MEDIUM"; - case enums::CLIMATE_FAN_HIGH: - return "CLIMATE_FAN_HIGH"; - case enums::CLIMATE_FAN_MIDDLE: - return "CLIMATE_FAN_MIDDLE"; - case enums::CLIMATE_FAN_FOCUS: - return "CLIMATE_FAN_FOCUS"; - case enums::CLIMATE_FAN_DIFFUSE: - return "CLIMATE_FAN_DIFFUSE"; - case enums::CLIMATE_FAN_QUIET: - return "CLIMATE_FAN_QUIET"; - default: - return "UNKNOWN"; - } -} -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP -template<> const char *proto_enum_to_string(enums::ClimateSwingMode value) { - switch (value) { - case enums::CLIMATE_SWING_OFF: - return "CLIMATE_SWING_OFF"; - case enums::CLIMATE_SWING_BOTH: - return "CLIMATE_SWING_BOTH"; - case enums::CLIMATE_SWING_VERTICAL: - return "CLIMATE_SWING_VERTICAL"; - case enums::CLIMATE_SWING_HORIZONTAL: - return "CLIMATE_SWING_HORIZONTAL"; - default: - return "UNKNOWN"; - } -} -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP -template<> const char *proto_enum_to_string(enums::ClimateAction value) { - switch (value) { - case enums::CLIMATE_ACTION_OFF: - return "CLIMATE_ACTION_OFF"; - case enums::CLIMATE_ACTION_COOLING: - return "CLIMATE_ACTION_COOLING"; - case enums::CLIMATE_ACTION_HEATING: - return "CLIMATE_ACTION_HEATING"; - case enums::CLIMATE_ACTION_IDLE: - return "CLIMATE_ACTION_IDLE"; - case enums::CLIMATE_ACTION_DRYING: - return "CLIMATE_ACTION_DRYING"; - case enums::CLIMATE_ACTION_FAN: - return "CLIMATE_ACTION_FAN"; - default: - return "UNKNOWN"; - } -} -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP -template<> const char *proto_enum_to_string(enums::ClimatePreset value) { - switch (value) { - case enums::CLIMATE_PRESET_NONE: - return "CLIMATE_PRESET_NONE"; - case enums::CLIMATE_PRESET_HOME: - return "CLIMATE_PRESET_HOME"; - case enums::CLIMATE_PRESET_AWAY: - return "CLIMATE_PRESET_AWAY"; - case enums::CLIMATE_PRESET_BOOST: - return "CLIMATE_PRESET_BOOST"; - case enums::CLIMATE_PRESET_COMFORT: - return "CLIMATE_PRESET_COMFORT"; - case enums::CLIMATE_PRESET_ECO: - return "CLIMATE_PRESET_ECO"; - case enums::CLIMATE_PRESET_SLEEP: - return "CLIMATE_PRESET_SLEEP"; - case enums::CLIMATE_PRESET_ACTIVITY: - return "CLIMATE_PRESET_ACTIVITY"; - default: - return "UNKNOWN"; - } -} -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP -template<> const char *proto_enum_to_string(enums::NumberMode value) { - switch (value) { - case enums::NUMBER_MODE_AUTO: - return "NUMBER_MODE_AUTO"; - case enums::NUMBER_MODE_BOX: - return "NUMBER_MODE_BOX"; - case enums::NUMBER_MODE_SLIDER: - return "NUMBER_MODE_SLIDER"; - default: - return "UNKNOWN"; - } -} -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP -template<> const char *proto_enum_to_string(enums::LockState value) { - switch (value) { - case enums::LOCK_STATE_NONE: - return "LOCK_STATE_NONE"; - case enums::LOCK_STATE_LOCKED: - return "LOCK_STATE_LOCKED"; - case enums::LOCK_STATE_UNLOCKED: - return "LOCK_STATE_UNLOCKED"; - case enums::LOCK_STATE_JAMMED: - return "LOCK_STATE_JAMMED"; - case enums::LOCK_STATE_LOCKING: - return "LOCK_STATE_LOCKING"; - case enums::LOCK_STATE_UNLOCKING: - return "LOCK_STATE_UNLOCKING"; - default: - return "UNKNOWN"; - } -} -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP -template<> const char *proto_enum_to_string(enums::LockCommand value) { - switch (value) { - case enums::LOCK_UNLOCK: - return "LOCK_UNLOCK"; - case enums::LOCK_LOCK: - return "LOCK_LOCK"; - case enums::LOCK_OPEN: - return "LOCK_OPEN"; - default: - return "UNKNOWN"; - } -} -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP -template<> const char *proto_enum_to_string(enums::MediaPlayerState value) { - switch (value) { - case enums::MEDIA_PLAYER_STATE_NONE: - return "MEDIA_PLAYER_STATE_NONE"; - case enums::MEDIA_PLAYER_STATE_IDLE: - return "MEDIA_PLAYER_STATE_IDLE"; - case enums::MEDIA_PLAYER_STATE_PLAYING: - return "MEDIA_PLAYER_STATE_PLAYING"; - case enums::MEDIA_PLAYER_STATE_PAUSED: - return "MEDIA_PLAYER_STATE_PAUSED"; - default: - return "UNKNOWN"; - } -} -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP -template<> const char *proto_enum_to_string(enums::MediaPlayerCommand value) { - switch (value) { - case enums::MEDIA_PLAYER_COMMAND_PLAY: - return "MEDIA_PLAYER_COMMAND_PLAY"; - case enums::MEDIA_PLAYER_COMMAND_PAUSE: - return "MEDIA_PLAYER_COMMAND_PAUSE"; - case enums::MEDIA_PLAYER_COMMAND_STOP: - return "MEDIA_PLAYER_COMMAND_STOP"; - case enums::MEDIA_PLAYER_COMMAND_MUTE: - return "MEDIA_PLAYER_COMMAND_MUTE"; - case enums::MEDIA_PLAYER_COMMAND_UNMUTE: - return "MEDIA_PLAYER_COMMAND_UNMUTE"; - default: - return "UNKNOWN"; - } -} -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP -template<> const char *proto_enum_to_string(enums::MediaPlayerFormatPurpose value) { - switch (value) { - case enums::MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT: - return "MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT"; - case enums::MEDIA_PLAYER_FORMAT_PURPOSE_ANNOUNCEMENT: - return "MEDIA_PLAYER_FORMAT_PURPOSE_ANNOUNCEMENT"; - default: - return "UNKNOWN"; - } -} -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP -template<> -const char *proto_enum_to_string(enums::BluetoothDeviceRequestType value) { - switch (value) { - case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT: - return "BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT"; - case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT: - return "BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT"; - case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_PAIR: - return "BLUETOOTH_DEVICE_REQUEST_TYPE_PAIR"; - case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_UNPAIR: - return "BLUETOOTH_DEVICE_REQUEST_TYPE_UNPAIR"; - case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITH_CACHE: - return "BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITH_CACHE"; - case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE: - return "BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE"; - case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CLEAR_CACHE: - return "BLUETOOTH_DEVICE_REQUEST_TYPE_CLEAR_CACHE"; - default: - return "UNKNOWN"; - } -} -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP -template<> const char *proto_enum_to_string(enums::BluetoothScannerState value) { - switch (value) { - case enums::BLUETOOTH_SCANNER_STATE_IDLE: - return "BLUETOOTH_SCANNER_STATE_IDLE"; - case enums::BLUETOOTH_SCANNER_STATE_STARTING: - return "BLUETOOTH_SCANNER_STATE_STARTING"; - case enums::BLUETOOTH_SCANNER_STATE_RUNNING: - return "BLUETOOTH_SCANNER_STATE_RUNNING"; - case enums::BLUETOOTH_SCANNER_STATE_FAILED: - return "BLUETOOTH_SCANNER_STATE_FAILED"; - case enums::BLUETOOTH_SCANNER_STATE_STOPPING: - return "BLUETOOTH_SCANNER_STATE_STOPPING"; - case enums::BLUETOOTH_SCANNER_STATE_STOPPED: - return "BLUETOOTH_SCANNER_STATE_STOPPED"; - default: - return "UNKNOWN"; - } -} -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP -template<> const char *proto_enum_to_string(enums::BluetoothScannerMode value) { - switch (value) { - case enums::BLUETOOTH_SCANNER_MODE_PASSIVE: - return "BLUETOOTH_SCANNER_MODE_PASSIVE"; - case enums::BLUETOOTH_SCANNER_MODE_ACTIVE: - return "BLUETOOTH_SCANNER_MODE_ACTIVE"; - default: - return "UNKNOWN"; - } -} -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP -template<> -const char *proto_enum_to_string(enums::VoiceAssistantSubscribeFlag value) { - switch (value) { - case enums::VOICE_ASSISTANT_SUBSCRIBE_NONE: - return "VOICE_ASSISTANT_SUBSCRIBE_NONE"; - case enums::VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO: - return "VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO"; - default: - return "UNKNOWN"; - } -} -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP -template<> const char *proto_enum_to_string(enums::VoiceAssistantRequestFlag value) { - switch (value) { - case enums::VOICE_ASSISTANT_REQUEST_NONE: - return "VOICE_ASSISTANT_REQUEST_NONE"; - case enums::VOICE_ASSISTANT_REQUEST_USE_VAD: - return "VOICE_ASSISTANT_REQUEST_USE_VAD"; - case enums::VOICE_ASSISTANT_REQUEST_USE_WAKE_WORD: - return "VOICE_ASSISTANT_REQUEST_USE_WAKE_WORD"; - default: - return "UNKNOWN"; - } -} -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP -template<> const char *proto_enum_to_string(enums::VoiceAssistantEvent value) { - switch (value) { - case enums::VOICE_ASSISTANT_ERROR: - return "VOICE_ASSISTANT_ERROR"; - case enums::VOICE_ASSISTANT_RUN_START: - return "VOICE_ASSISTANT_RUN_START"; - case enums::VOICE_ASSISTANT_RUN_END: - return "VOICE_ASSISTANT_RUN_END"; - case enums::VOICE_ASSISTANT_STT_START: - return "VOICE_ASSISTANT_STT_START"; - case enums::VOICE_ASSISTANT_STT_END: - return "VOICE_ASSISTANT_STT_END"; - case enums::VOICE_ASSISTANT_INTENT_START: - return "VOICE_ASSISTANT_INTENT_START"; - case enums::VOICE_ASSISTANT_INTENT_END: - return "VOICE_ASSISTANT_INTENT_END"; - case enums::VOICE_ASSISTANT_TTS_START: - return "VOICE_ASSISTANT_TTS_START"; - case enums::VOICE_ASSISTANT_TTS_END: - return "VOICE_ASSISTANT_TTS_END"; - case enums::VOICE_ASSISTANT_WAKE_WORD_START: - return "VOICE_ASSISTANT_WAKE_WORD_START"; - case enums::VOICE_ASSISTANT_WAKE_WORD_END: - return "VOICE_ASSISTANT_WAKE_WORD_END"; - case enums::VOICE_ASSISTANT_STT_VAD_START: - return "VOICE_ASSISTANT_STT_VAD_START"; - case enums::VOICE_ASSISTANT_STT_VAD_END: - return "VOICE_ASSISTANT_STT_VAD_END"; - case enums::VOICE_ASSISTANT_TTS_STREAM_START: - return "VOICE_ASSISTANT_TTS_STREAM_START"; - case enums::VOICE_ASSISTANT_TTS_STREAM_END: - return "VOICE_ASSISTANT_TTS_STREAM_END"; - case enums::VOICE_ASSISTANT_INTENT_PROGRESS: - return "VOICE_ASSISTANT_INTENT_PROGRESS"; - default: - return "UNKNOWN"; - } -} -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP -template<> const char *proto_enum_to_string(enums::VoiceAssistantTimerEvent value) { - switch (value) { - case enums::VOICE_ASSISTANT_TIMER_STARTED: - return "VOICE_ASSISTANT_TIMER_STARTED"; - case enums::VOICE_ASSISTANT_TIMER_UPDATED: - return "VOICE_ASSISTANT_TIMER_UPDATED"; - case enums::VOICE_ASSISTANT_TIMER_CANCELLED: - return "VOICE_ASSISTANT_TIMER_CANCELLED"; - case enums::VOICE_ASSISTANT_TIMER_FINISHED: - return "VOICE_ASSISTANT_TIMER_FINISHED"; - default: - return "UNKNOWN"; - } -} -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP -template<> const char *proto_enum_to_string(enums::AlarmControlPanelState value) { - switch (value) { - case enums::ALARM_STATE_DISARMED: - return "ALARM_STATE_DISARMED"; - case enums::ALARM_STATE_ARMED_HOME: - return "ALARM_STATE_ARMED_HOME"; - case enums::ALARM_STATE_ARMED_AWAY: - return "ALARM_STATE_ARMED_AWAY"; - case enums::ALARM_STATE_ARMED_NIGHT: - return "ALARM_STATE_ARMED_NIGHT"; - case enums::ALARM_STATE_ARMED_VACATION: - return "ALARM_STATE_ARMED_VACATION"; - case enums::ALARM_STATE_ARMED_CUSTOM_BYPASS: - return "ALARM_STATE_ARMED_CUSTOM_BYPASS"; - case enums::ALARM_STATE_PENDING: - return "ALARM_STATE_PENDING"; - case enums::ALARM_STATE_ARMING: - return "ALARM_STATE_ARMING"; - case enums::ALARM_STATE_DISARMING: - return "ALARM_STATE_DISARMING"; - case enums::ALARM_STATE_TRIGGERED: - return "ALARM_STATE_TRIGGERED"; - default: - return "UNKNOWN"; - } -} -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP -template<> -const char *proto_enum_to_string(enums::AlarmControlPanelStateCommand value) { - switch (value) { - case enums::ALARM_CONTROL_PANEL_DISARM: - return "ALARM_CONTROL_PANEL_DISARM"; - case enums::ALARM_CONTROL_PANEL_ARM_AWAY: - return "ALARM_CONTROL_PANEL_ARM_AWAY"; - case enums::ALARM_CONTROL_PANEL_ARM_HOME: - return "ALARM_CONTROL_PANEL_ARM_HOME"; - case enums::ALARM_CONTROL_PANEL_ARM_NIGHT: - return "ALARM_CONTROL_PANEL_ARM_NIGHT"; - case enums::ALARM_CONTROL_PANEL_ARM_VACATION: - return "ALARM_CONTROL_PANEL_ARM_VACATION"; - case enums::ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS: - return "ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS"; - case enums::ALARM_CONTROL_PANEL_TRIGGER: - return "ALARM_CONTROL_PANEL_TRIGGER"; - default: - return "UNKNOWN"; - } -} -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP -template<> const char *proto_enum_to_string(enums::TextMode value) { - switch (value) { - case enums::TEXT_MODE_TEXT: - return "TEXT_MODE_TEXT"; - case enums::TEXT_MODE_PASSWORD: - return "TEXT_MODE_PASSWORD"; - default: - return "UNKNOWN"; - } -} -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP -template<> const char *proto_enum_to_string(enums::ValveOperation value) { - switch (value) { - case enums::VALVE_OPERATION_IDLE: - return "VALVE_OPERATION_IDLE"; - case enums::VALVE_OPERATION_IS_OPENING: - return "VALVE_OPERATION_IS_OPENING"; - case enums::VALVE_OPERATION_IS_CLOSING: - return "VALVE_OPERATION_IS_CLOSING"; - default: - return "UNKNOWN"; - } -} -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP -template<> const char *proto_enum_to_string(enums::UpdateCommand value) { - switch (value) { - case enums::UPDATE_COMMAND_NONE: - return "UPDATE_COMMAND_NONE"; - case enums::UPDATE_COMMAND_UPDATE: - return "UPDATE_COMMAND_UPDATE"; - case enums::UPDATE_COMMAND_CHECK: - return "UPDATE_COMMAND_CHECK"; - default: - return "UNKNOWN"; - } -} -#endif - bool HelloRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -665,26 +42,6 @@ void HelloRequest::calculate_size(uint32_t &total_size) const { ProtoSize::add_uint32_field(total_size, 1, this->api_version_major, false); ProtoSize::add_uint32_field(total_size, 1, this->api_version_minor, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void HelloRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("HelloRequest {\n"); - out.append(" client_info: "); - out.append("'").append(this->client_info).append("'"); - out.append("\n"); - - out.append(" api_version_major: "); - sprintf(buffer, "%" PRIu32, this->api_version_major); - out.append(buffer); - out.append("\n"); - - out.append(" api_version_minor: "); - sprintf(buffer, "%" PRIu32, this->api_version_minor); - out.append(buffer); - out.append("\n"); - out.append("}"); -} -#endif bool HelloResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -725,30 +82,6 @@ void HelloResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->server_info, false); ProtoSize::add_string_field(total_size, 1, this->name, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void HelloResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("HelloResponse {\n"); - out.append(" api_version_major: "); - sprintf(buffer, "%" PRIu32, this->api_version_major); - out.append(buffer); - out.append("\n"); - - out.append(" api_version_minor: "); - sprintf(buffer, "%" PRIu32, this->api_version_minor); - out.append(buffer); - out.append("\n"); - - out.append(" server_info: "); - out.append("'").append(this->server_info).append("'"); - out.append("\n"); - - out.append(" name: "); - out.append("'").append(this->name).append("'"); - out.append("\n"); - out.append("}"); -} -#endif bool ConnectRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 1: { @@ -763,16 +96,6 @@ void ConnectRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_strin void ConnectRequest::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->password, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void ConnectRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ConnectRequest {\n"); - out.append(" password: "); - out.append("'").append(this->password).append("'"); - out.append("\n"); - out.append("}"); -} -#endif bool ConnectResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -787,41 +110,68 @@ void ConnectResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool void ConnectResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 1, this->invalid_password, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void ConnectResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ConnectResponse {\n"); - out.append(" invalid_password: "); - out.append(YESNO(this->invalid_password)); - out.append("\n"); - out.append("}"); +bool AreaInfo::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: { + this->area_id = value.as_uint32(); + return true; + } + default: + return false; + } +} +bool AreaInfo::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 2: { + this->name = value.as_string(); + return true; + } + default: + return false; + } +} +void AreaInfo::encode(ProtoWriteBuffer buffer) const { + buffer.encode_uint32(1, this->area_id); + buffer.encode_string(2, this->name); +} +void AreaInfo::calculate_size(uint32_t &total_size) const { + ProtoSize::add_uint32_field(total_size, 1, this->area_id, false); + ProtoSize::add_string_field(total_size, 1, this->name, false); +} +bool DeviceInfo::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: { + this->device_id = value.as_uint32(); + return true; + } + case 3: { + this->area_id = value.as_uint32(); + return true; + } + default: + return false; + } +} +bool DeviceInfo::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 2: { + this->name = value.as_string(); + return true; + } + default: + return false; + } +} +void DeviceInfo::encode(ProtoWriteBuffer buffer) const { + buffer.encode_uint32(1, this->device_id); + buffer.encode_string(2, this->name); + buffer.encode_uint32(3, this->area_id); +} +void DeviceInfo::calculate_size(uint32_t &total_size) const { + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); + ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_uint32_field(total_size, 1, this->area_id, false); } -#endif -void DisconnectRequest::encode(ProtoWriteBuffer buffer) const {} -void DisconnectRequest::calculate_size(uint32_t &total_size) const {} -#ifdef HAS_PROTO_MESSAGE_DUMP -void DisconnectRequest::dump_to(std::string &out) const { out.append("DisconnectRequest {}"); } -#endif -void DisconnectResponse::encode(ProtoWriteBuffer buffer) const {} -void DisconnectResponse::calculate_size(uint32_t &total_size) const {} -#ifdef HAS_PROTO_MESSAGE_DUMP -void DisconnectResponse::dump_to(std::string &out) const { out.append("DisconnectResponse {}"); } -#endif -void PingRequest::encode(ProtoWriteBuffer buffer) const {} -void PingRequest::calculate_size(uint32_t &total_size) const {} -#ifdef HAS_PROTO_MESSAGE_DUMP -void PingRequest::dump_to(std::string &out) const { out.append("PingRequest {}"); } -#endif -void PingResponse::encode(ProtoWriteBuffer buffer) const {} -void PingResponse::calculate_size(uint32_t &total_size) const {} -#ifdef HAS_PROTO_MESSAGE_DUMP -void PingResponse::dump_to(std::string &out) const { out.append("PingResponse {}"); } -#endif -void DeviceInfoRequest::encode(ProtoWriteBuffer buffer) const {} -void DeviceInfoRequest::calculate_size(uint32_t &total_size) const {} -#ifdef HAS_PROTO_MESSAGE_DUMP -void DeviceInfoRequest::dump_to(std::string &out) const { out.append("DeviceInfoRequest {}"); } -#endif bool DeviceInfoResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -906,6 +256,18 @@ bool DeviceInfoResponse::decode_length(uint32_t field_id, ProtoLengthDelimited v this->bluetooth_mac_address = value.as_string(); return true; } + case 20: { + this->devices.push_back(value.as_message()); + return true; + } + case 21: { + this->areas.push_back(value.as_message()); + return true; + } + case 22: { + this->area = value.as_message(); + return true; + } default: return false; } @@ -930,6 +292,13 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(16, this->suggested_area); buffer.encode_string(18, this->bluetooth_mac_address); buffer.encode_bool(19, this->api_encryption_supported); + for (auto &it : this->devices) { + buffer.encode_message(20, it, true); + } + for (auto &it : this->areas) { + buffer.encode_message(21, it, true); + } + buffer.encode_message(22, this->area); } void DeviceInfoResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 1, this->uses_password, false); @@ -951,109 +320,11 @@ void DeviceInfoResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 2, this->suggested_area, false); ProtoSize::add_string_field(total_size, 2, this->bluetooth_mac_address, false); ProtoSize::add_bool_field(total_size, 2, this->api_encryption_supported, false); + ProtoSize::add_repeated_message(total_size, 2, this->devices); + ProtoSize::add_repeated_message(total_size, 2, this->areas); + ProtoSize::add_message_object(total_size, 2, this->area, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void DeviceInfoResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("DeviceInfoResponse {\n"); - out.append(" uses_password: "); - out.append(YESNO(this->uses_password)); - out.append("\n"); - - out.append(" name: "); - out.append("'").append(this->name).append("'"); - out.append("\n"); - - out.append(" mac_address: "); - out.append("'").append(this->mac_address).append("'"); - out.append("\n"); - - out.append(" esphome_version: "); - out.append("'").append(this->esphome_version).append("'"); - out.append("\n"); - - out.append(" compilation_time: "); - out.append("'").append(this->compilation_time).append("'"); - out.append("\n"); - - out.append(" model: "); - out.append("'").append(this->model).append("'"); - out.append("\n"); - - out.append(" has_deep_sleep: "); - out.append(YESNO(this->has_deep_sleep)); - out.append("\n"); - - out.append(" project_name: "); - out.append("'").append(this->project_name).append("'"); - out.append("\n"); - - out.append(" project_version: "); - out.append("'").append(this->project_version).append("'"); - out.append("\n"); - - out.append(" webserver_port: "); - sprintf(buffer, "%" PRIu32, this->webserver_port); - out.append(buffer); - out.append("\n"); - - out.append(" legacy_bluetooth_proxy_version: "); - sprintf(buffer, "%" PRIu32, this->legacy_bluetooth_proxy_version); - out.append(buffer); - out.append("\n"); - - out.append(" bluetooth_proxy_feature_flags: "); - sprintf(buffer, "%" PRIu32, this->bluetooth_proxy_feature_flags); - out.append(buffer); - out.append("\n"); - - out.append(" manufacturer: "); - out.append("'").append(this->manufacturer).append("'"); - out.append("\n"); - - out.append(" friendly_name: "); - out.append("'").append(this->friendly_name).append("'"); - out.append("\n"); - - out.append(" legacy_voice_assistant_version: "); - sprintf(buffer, "%" PRIu32, this->legacy_voice_assistant_version); - out.append(buffer); - out.append("\n"); - - out.append(" voice_assistant_feature_flags: "); - sprintf(buffer, "%" PRIu32, this->voice_assistant_feature_flags); - out.append(buffer); - out.append("\n"); - - out.append(" suggested_area: "); - out.append("'").append(this->suggested_area).append("'"); - out.append("\n"); - - out.append(" bluetooth_mac_address: "); - out.append("'").append(this->bluetooth_mac_address).append("'"); - out.append("\n"); - - out.append(" api_encryption_supported: "); - out.append(YESNO(this->api_encryption_supported)); - out.append("\n"); - out.append("}"); -} -#endif -void ListEntitiesRequest::encode(ProtoWriteBuffer buffer) const {} -void ListEntitiesRequest::calculate_size(uint32_t &total_size) const {} -#ifdef HAS_PROTO_MESSAGE_DUMP -void ListEntitiesRequest::dump_to(std::string &out) const { out.append("ListEntitiesRequest {}"); } -#endif -void ListEntitiesDoneResponse::encode(ProtoWriteBuffer buffer) const {} -void ListEntitiesDoneResponse::calculate_size(uint32_t &total_size) const {} -#ifdef HAS_PROTO_MESSAGE_DUMP -void ListEntitiesDoneResponse::dump_to(std::string &out) const { out.append("ListEntitiesDoneResponse {}"); } -#endif -void SubscribeStatesRequest::encode(ProtoWriteBuffer buffer) const {} -void SubscribeStatesRequest::calculate_size(uint32_t &total_size) const {} -#ifdef HAS_PROTO_MESSAGE_DUMP -void SubscribeStatesRequest::dump_to(std::string &out) const { out.append("SubscribeStatesRequest {}"); } -#endif +#ifdef USE_BINARY_SENSOR bool ListEntitiesBinarySensorResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 6: { @@ -1068,6 +339,10 @@ bool ListEntitiesBinarySensorResponse::decode_varint(uint32_t field_id, ProtoVar this->entity_category = value.as_enum(); return true; } + case 10: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -1118,6 +393,7 @@ void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(7, this->disabled_by_default); buffer.encode_string(8, this->icon); buffer.encode_enum(9, this->entity_category); + buffer.encode_uint32(10, this->device_id); } void ListEntitiesBinarySensorResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -1129,50 +405,8 @@ void ListEntitiesBinarySensorResponse::calculate_size(uint32_t &total_size) cons ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); ProtoSize::add_string_field(total_size, 1, this->icon, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void ListEntitiesBinarySensorResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesBinarySensorResponse {\n"); - out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); - out.append("\n"); - - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - out.append("'").append(this->name).append("'"); - out.append("\n"); - - out.append(" unique_id: "); - out.append("'").append(this->unique_id).append("'"); - out.append("\n"); - - out.append(" device_class: "); - out.append("'").append(this->device_class).append("'"); - out.append("\n"); - - out.append(" is_status_binary_sensor: "); - out.append(YESNO(this->is_status_binary_sensor)); - out.append("\n"); - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" icon: "); - out.append("'").append(this->icon).append("'"); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - out.append("}"); -} -#endif bool BinarySensorStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -1183,6 +417,10 @@ bool BinarySensorStateResponse::decode_varint(uint32_t field_id, ProtoVarInt val this->missing_state = value.as_bool(); return true; } + case 4: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -1201,31 +439,16 @@ void BinarySensorStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->state); buffer.encode_bool(3, this->missing_state); + buffer.encode_uint32(4, this->device_id); } void BinarySensorStateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); ProtoSize::add_bool_field(total_size, 1, this->state, false); ProtoSize::add_bool_field(total_size, 1, this->missing_state, false); -} -#ifdef HAS_PROTO_MESSAGE_DUMP -void BinarySensorStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BinarySensorStateResponse {\n"); - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - out.append(YESNO(this->state)); - out.append("\n"); - - out.append(" missing_state: "); - out.append(YESNO(this->missing_state)); - out.append("\n"); - out.append("}"); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } #endif +#ifdef USE_COVER bool ListEntitiesCoverResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 5: { @@ -1252,6 +475,10 @@ bool ListEntitiesCoverResponse::decode_varint(uint32_t field_id, ProtoVarInt val this->supports_stop = value.as_bool(); return true; } + case 13: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -1305,6 +532,7 @@ void ListEntitiesCoverResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(10, this->icon); buffer.encode_enum(11, this->entity_category); buffer.encode_bool(12, this->supports_stop); + buffer.encode_uint32(13, this->device_id); } void ListEntitiesCoverResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -1319,62 +547,8 @@ void ListEntitiesCoverResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->icon, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); ProtoSize::add_bool_field(total_size, 1, this->supports_stop, false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void ListEntitiesCoverResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesCoverResponse {\n"); - out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); - out.append("\n"); - - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - out.append("'").append(this->name).append("'"); - out.append("\n"); - - out.append(" unique_id: "); - out.append("'").append(this->unique_id).append("'"); - out.append("\n"); - - out.append(" assumed_state: "); - out.append(YESNO(this->assumed_state)); - out.append("\n"); - - out.append(" supports_position: "); - out.append(YESNO(this->supports_position)); - out.append("\n"); - - out.append(" supports_tilt: "); - out.append(YESNO(this->supports_tilt)); - out.append("\n"); - - out.append(" device_class: "); - out.append("'").append(this->device_class).append("'"); - out.append("\n"); - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" icon: "); - out.append("'").append(this->icon).append("'"); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - - out.append(" supports_stop: "); - out.append(YESNO(this->supports_stop)); - out.append("\n"); - out.append("}"); -} -#endif bool CoverStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -1385,6 +559,10 @@ bool CoverStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { this->current_operation = value.as_enum(); return true; } + case 6: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -1413,6 +591,7 @@ void CoverStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(3, this->position); buffer.encode_float(4, this->tilt); buffer.encode_enum(5, this->current_operation); + buffer.encode_uint32(6, this->device_id); } void CoverStateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); @@ -1420,36 +599,8 @@ void CoverStateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_fixed_field<4>(total_size, 1, this->position != 0.0f, false); ProtoSize::add_fixed_field<4>(total_size, 1, this->tilt != 0.0f, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->current_operation), false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void CoverStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("CoverStateResponse {\n"); - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" legacy_state: "); - out.append(proto_enum_to_string(this->legacy_state)); - out.append("\n"); - - out.append(" position: "); - sprintf(buffer, "%g", this->position); - out.append(buffer); - out.append("\n"); - - out.append(" tilt: "); - sprintf(buffer, "%g", this->tilt); - out.append(buffer); - out.append("\n"); - - out.append(" current_operation: "); - out.append(proto_enum_to_string(this->current_operation)); - out.append("\n"); - out.append("}"); -} -#endif bool CoverCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -1514,47 +665,8 @@ void CoverCommandRequest::calculate_size(uint32_t &total_size) const { ProtoSize::add_fixed_field<4>(total_size, 1, this->tilt != 0.0f, false); ProtoSize::add_bool_field(total_size, 1, this->stop, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void CoverCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("CoverCommandRequest {\n"); - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" has_legacy_command: "); - out.append(YESNO(this->has_legacy_command)); - out.append("\n"); - - out.append(" legacy_command: "); - out.append(proto_enum_to_string(this->legacy_command)); - out.append("\n"); - - out.append(" has_position: "); - out.append(YESNO(this->has_position)); - out.append("\n"); - - out.append(" position: "); - sprintf(buffer, "%g", this->position); - out.append(buffer); - out.append("\n"); - - out.append(" has_tilt: "); - out.append(YESNO(this->has_tilt)); - out.append("\n"); - - out.append(" tilt: "); - sprintf(buffer, "%g", this->tilt); - out.append(buffer); - out.append("\n"); - - out.append(" stop: "); - out.append(YESNO(this->stop)); - out.append("\n"); - out.append("}"); -} #endif +#ifdef USE_FAN bool ListEntitiesFanResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 5: { @@ -1581,6 +693,10 @@ bool ListEntitiesFanResponse::decode_varint(uint32_t field_id, ProtoVarInt value this->entity_category = value.as_enum(); return true; } + case 13: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -1636,6 +752,7 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const { for (auto &it : this->supported_preset_modes) { buffer.encode_string(12, it, true); } + buffer.encode_uint32(13, this->device_id); } void ListEntitiesFanResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -1654,65 +771,8 @@ void ListEntitiesFanResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, it, true); } } + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void ListEntitiesFanResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesFanResponse {\n"); - out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); - out.append("\n"); - - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - out.append("'").append(this->name).append("'"); - out.append("\n"); - - out.append(" unique_id: "); - out.append("'").append(this->unique_id).append("'"); - out.append("\n"); - - out.append(" supports_oscillation: "); - out.append(YESNO(this->supports_oscillation)); - out.append("\n"); - - out.append(" supports_speed: "); - out.append(YESNO(this->supports_speed)); - out.append("\n"); - - out.append(" supports_direction: "); - out.append(YESNO(this->supports_direction)); - out.append("\n"); - - out.append(" supported_speed_count: "); - sprintf(buffer, "%" PRId32, this->supported_speed_count); - out.append(buffer); - out.append("\n"); - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" icon: "); - out.append("'").append(this->icon).append("'"); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - - for (const auto &it : this->supported_preset_modes) { - out.append(" supported_preset_modes: "); - out.append("'").append(it).append("'"); - out.append("\n"); - } - out.append("}"); -} -#endif bool FanStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -1735,6 +795,10 @@ bool FanStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { this->speed_level = value.as_int32(); return true; } + case 8: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -1767,6 +831,7 @@ void FanStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_enum(5, this->direction); buffer.encode_int32(6, this->speed_level); buffer.encode_string(7, this->preset_mode); + buffer.encode_uint32(8, this->device_id); } void FanStateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); @@ -1776,43 +841,8 @@ void FanStateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_enum_field(total_size, 1, static_cast(this->direction), false); ProtoSize::add_int32_field(total_size, 1, this->speed_level, false); ProtoSize::add_string_field(total_size, 1, this->preset_mode, false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void FanStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("FanStateResponse {\n"); - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - out.append(YESNO(this->state)); - out.append("\n"); - - out.append(" oscillating: "); - out.append(YESNO(this->oscillating)); - out.append("\n"); - - out.append(" speed: "); - out.append(proto_enum_to_string(this->speed)); - out.append("\n"); - - out.append(" direction: "); - out.append(proto_enum_to_string(this->direction)); - out.append("\n"); - - out.append(" speed_level: "); - sprintf(buffer, "%" PRId32, this->speed_level); - out.append(buffer); - out.append("\n"); - - out.append(" preset_mode: "); - out.append("'").append(this->preset_mode).append("'"); - out.append("\n"); - out.append("}"); -} -#endif bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -1913,66 +943,8 @@ void FanCommandRequest::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 1, this->has_preset_mode, false); ProtoSize::add_string_field(total_size, 1, this->preset_mode, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void FanCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("FanCommandRequest {\n"); - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" has_state: "); - out.append(YESNO(this->has_state)); - out.append("\n"); - - out.append(" state: "); - out.append(YESNO(this->state)); - out.append("\n"); - - out.append(" has_speed: "); - out.append(YESNO(this->has_speed)); - out.append("\n"); - - out.append(" speed: "); - out.append(proto_enum_to_string(this->speed)); - out.append("\n"); - - out.append(" has_oscillating: "); - out.append(YESNO(this->has_oscillating)); - out.append("\n"); - - out.append(" oscillating: "); - out.append(YESNO(this->oscillating)); - out.append("\n"); - - out.append(" has_direction: "); - out.append(YESNO(this->has_direction)); - out.append("\n"); - - out.append(" direction: "); - out.append(proto_enum_to_string(this->direction)); - out.append("\n"); - - out.append(" has_speed_level: "); - out.append(YESNO(this->has_speed_level)); - out.append("\n"); - - out.append(" speed_level: "); - sprintf(buffer, "%" PRId32, this->speed_level); - out.append(buffer); - out.append("\n"); - - out.append(" has_preset_mode: "); - out.append(YESNO(this->has_preset_mode)); - out.append("\n"); - - out.append(" preset_mode: "); - out.append("'").append(this->preset_mode).append("'"); - out.append("\n"); - out.append("}"); -} #endif +#ifdef USE_LIGHT bool ListEntitiesLightResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 12: { @@ -2003,6 +975,10 @@ bool ListEntitiesLightResponse::decode_varint(uint32_t field_id, ProtoVarInt val this->entity_category = value.as_enum(); return true; } + case 16: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -2071,6 +1047,7 @@ void ListEntitiesLightResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(13, this->disabled_by_default); buffer.encode_string(14, this->icon); buffer.encode_enum(15, this->entity_category); + buffer.encode_uint32(16, this->device_id); } void ListEntitiesLightResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -2096,80 +1073,8 @@ void ListEntitiesLightResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); ProtoSize::add_string_field(total_size, 1, this->icon, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); + ProtoSize::add_uint32_field(total_size, 2, this->device_id, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void ListEntitiesLightResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesLightResponse {\n"); - out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); - out.append("\n"); - - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - out.append("'").append(this->name).append("'"); - out.append("\n"); - - out.append(" unique_id: "); - out.append("'").append(this->unique_id).append("'"); - out.append("\n"); - - for (const auto &it : this->supported_color_modes) { - out.append(" supported_color_modes: "); - out.append(proto_enum_to_string(it)); - out.append("\n"); - } - - out.append(" legacy_supports_brightness: "); - out.append(YESNO(this->legacy_supports_brightness)); - out.append("\n"); - - out.append(" legacy_supports_rgb: "); - out.append(YESNO(this->legacy_supports_rgb)); - out.append("\n"); - - out.append(" legacy_supports_white_value: "); - out.append(YESNO(this->legacy_supports_white_value)); - out.append("\n"); - - out.append(" legacy_supports_color_temperature: "); - out.append(YESNO(this->legacy_supports_color_temperature)); - out.append("\n"); - - out.append(" min_mireds: "); - sprintf(buffer, "%g", this->min_mireds); - out.append(buffer); - out.append("\n"); - - out.append(" max_mireds: "); - sprintf(buffer, "%g", this->max_mireds); - out.append(buffer); - out.append("\n"); - - for (const auto &it : this->effects) { - out.append(" effects: "); - out.append("'").append(it).append("'"); - out.append("\n"); - } - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" icon: "); - out.append("'").append(this->icon).append("'"); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - out.append("}"); -} -#endif bool LightStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -2180,6 +1085,10 @@ bool LightStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { this->color_mode = value.as_enum(); return true; } + case 14: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -2254,6 +1163,7 @@ void LightStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(12, this->cold_white); buffer.encode_float(13, this->warm_white); buffer.encode_string(9, this->effect); + buffer.encode_uint32(14, this->device_id); } void LightStateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); @@ -2269,75 +1179,8 @@ void LightStateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_fixed_field<4>(total_size, 1, this->cold_white != 0.0f, false); ProtoSize::add_fixed_field<4>(total_size, 1, this->warm_white != 0.0f, false); ProtoSize::add_string_field(total_size, 1, this->effect, false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void LightStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("LightStateResponse {\n"); - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - out.append(YESNO(this->state)); - out.append("\n"); - - out.append(" brightness: "); - sprintf(buffer, "%g", this->brightness); - out.append(buffer); - out.append("\n"); - - out.append(" color_mode: "); - out.append(proto_enum_to_string(this->color_mode)); - out.append("\n"); - - out.append(" color_brightness: "); - sprintf(buffer, "%g", this->color_brightness); - out.append(buffer); - out.append("\n"); - - out.append(" red: "); - sprintf(buffer, "%g", this->red); - out.append(buffer); - out.append("\n"); - - out.append(" green: "); - sprintf(buffer, "%g", this->green); - out.append(buffer); - out.append("\n"); - - out.append(" blue: "); - sprintf(buffer, "%g", this->blue); - out.append(buffer); - out.append("\n"); - - out.append(" white: "); - sprintf(buffer, "%g", this->white); - out.append(buffer); - out.append("\n"); - - out.append(" color_temperature: "); - sprintf(buffer, "%g", this->color_temperature); - out.append(buffer); - out.append("\n"); - - out.append(" cold_white: "); - sprintf(buffer, "%g", this->cold_white); - out.append(buffer); - out.append("\n"); - - out.append(" warm_white: "); - sprintf(buffer, "%g", this->warm_white); - out.append(buffer); - out.append("\n"); - - out.append(" effect: "); - out.append("'").append(this->effect).append("'"); - out.append("\n"); - out.append("}"); -} -#endif bool LightCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -2522,132 +1365,8 @@ void LightCommandRequest::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 2, this->has_effect, false); ProtoSize::add_string_field(total_size, 2, this->effect, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void LightCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("LightCommandRequest {\n"); - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" has_state: "); - out.append(YESNO(this->has_state)); - out.append("\n"); - - out.append(" state: "); - out.append(YESNO(this->state)); - out.append("\n"); - - out.append(" has_brightness: "); - out.append(YESNO(this->has_brightness)); - out.append("\n"); - - out.append(" brightness: "); - sprintf(buffer, "%g", this->brightness); - out.append(buffer); - out.append("\n"); - - out.append(" has_color_mode: "); - out.append(YESNO(this->has_color_mode)); - out.append("\n"); - - out.append(" color_mode: "); - out.append(proto_enum_to_string(this->color_mode)); - out.append("\n"); - - out.append(" has_color_brightness: "); - out.append(YESNO(this->has_color_brightness)); - out.append("\n"); - - out.append(" color_brightness: "); - sprintf(buffer, "%g", this->color_brightness); - out.append(buffer); - out.append("\n"); - - out.append(" has_rgb: "); - out.append(YESNO(this->has_rgb)); - out.append("\n"); - - out.append(" red: "); - sprintf(buffer, "%g", this->red); - out.append(buffer); - out.append("\n"); - - out.append(" green: "); - sprintf(buffer, "%g", this->green); - out.append(buffer); - out.append("\n"); - - out.append(" blue: "); - sprintf(buffer, "%g", this->blue); - out.append(buffer); - out.append("\n"); - - out.append(" has_white: "); - out.append(YESNO(this->has_white)); - out.append("\n"); - - out.append(" white: "); - sprintf(buffer, "%g", this->white); - out.append(buffer); - out.append("\n"); - - out.append(" has_color_temperature: "); - out.append(YESNO(this->has_color_temperature)); - out.append("\n"); - - out.append(" color_temperature: "); - sprintf(buffer, "%g", this->color_temperature); - out.append(buffer); - out.append("\n"); - - out.append(" has_cold_white: "); - out.append(YESNO(this->has_cold_white)); - out.append("\n"); - - out.append(" cold_white: "); - sprintf(buffer, "%g", this->cold_white); - out.append(buffer); - out.append("\n"); - - out.append(" has_warm_white: "); - out.append(YESNO(this->has_warm_white)); - out.append("\n"); - - out.append(" warm_white: "); - sprintf(buffer, "%g", this->warm_white); - out.append(buffer); - out.append("\n"); - - out.append(" has_transition_length: "); - out.append(YESNO(this->has_transition_length)); - out.append("\n"); - - out.append(" transition_length: "); - sprintf(buffer, "%" PRIu32, this->transition_length); - out.append(buffer); - out.append("\n"); - - out.append(" has_flash_length: "); - out.append(YESNO(this->has_flash_length)); - out.append("\n"); - - out.append(" flash_length: "); - sprintf(buffer, "%" PRIu32, this->flash_length); - out.append(buffer); - out.append("\n"); - - out.append(" has_effect: "); - out.append(YESNO(this->has_effect)); - out.append("\n"); - - out.append(" effect: "); - out.append("'").append(this->effect).append("'"); - out.append("\n"); - out.append("}"); -} #endif +#ifdef USE_SENSOR bool ListEntitiesSensorResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 7: { @@ -2674,6 +1393,10 @@ bool ListEntitiesSensorResponse::decode_varint(uint32_t field_id, ProtoVarInt va this->entity_category = value.as_enum(); return true; } + case 14: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -2732,6 +1455,7 @@ void ListEntitiesSensorResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_enum(11, this->legacy_last_reset_type); buffer.encode_bool(12, this->disabled_by_default); buffer.encode_enum(13, this->entity_category); + buffer.encode_uint32(14, this->device_id); } void ListEntitiesSensorResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -2747,73 +1471,18 @@ void ListEntitiesSensorResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_enum_field(total_size, 1, static_cast(this->legacy_last_reset_type), false); ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void ListEntitiesSensorResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesSensorResponse {\n"); - out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); - out.append("\n"); - - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - out.append("'").append(this->name).append("'"); - out.append("\n"); - - out.append(" unique_id: "); - out.append("'").append(this->unique_id).append("'"); - out.append("\n"); - - out.append(" icon: "); - out.append("'").append(this->icon).append("'"); - out.append("\n"); - - out.append(" unit_of_measurement: "); - out.append("'").append(this->unit_of_measurement).append("'"); - out.append("\n"); - - out.append(" accuracy_decimals: "); - sprintf(buffer, "%" PRId32, this->accuracy_decimals); - out.append(buffer); - out.append("\n"); - - out.append(" force_update: "); - out.append(YESNO(this->force_update)); - out.append("\n"); - - out.append(" device_class: "); - out.append("'").append(this->device_class).append("'"); - out.append("\n"); - - out.append(" state_class: "); - out.append(proto_enum_to_string(this->state_class)); - out.append("\n"); - - out.append(" legacy_last_reset_type: "); - out.append(proto_enum_to_string(this->legacy_last_reset_type)); - out.append("\n"); - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - out.append("}"); -} -#endif bool SensorStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 3: { this->missing_state = value.as_bool(); return true; } + case 4: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -2836,32 +1505,16 @@ void SensorStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_float(2, this->state); buffer.encode_bool(3, this->missing_state); + buffer.encode_uint32(4, this->device_id); } void SensorStateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); ProtoSize::add_fixed_field<4>(total_size, 1, this->state != 0.0f, false); ProtoSize::add_bool_field(total_size, 1, this->missing_state, false); -} -#ifdef HAS_PROTO_MESSAGE_DUMP -void SensorStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("SensorStateResponse {\n"); - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - sprintf(buffer, "%g", this->state); - out.append(buffer); - out.append("\n"); - - out.append(" missing_state: "); - out.append(YESNO(this->missing_state)); - out.append("\n"); - out.append("}"); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } #endif +#ifdef USE_SWITCH bool ListEntitiesSwitchResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 6: { @@ -2876,6 +1529,10 @@ bool ListEntitiesSwitchResponse::decode_varint(uint32_t field_id, ProtoVarInt va this->entity_category = value.as_enum(); return true; } + case 10: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -2926,6 +1583,7 @@ void ListEntitiesSwitchResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(7, this->disabled_by_default); buffer.encode_enum(8, this->entity_category); buffer.encode_string(9, this->device_class); + buffer.encode_uint32(10, this->device_id); } void ListEntitiesSwitchResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -2937,56 +1595,18 @@ void ListEntitiesSwitchResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); ProtoSize::add_string_field(total_size, 1, this->device_class, false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void ListEntitiesSwitchResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesSwitchResponse {\n"); - out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); - out.append("\n"); - - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - out.append("'").append(this->name).append("'"); - out.append("\n"); - - out.append(" unique_id: "); - out.append("'").append(this->unique_id).append("'"); - out.append("\n"); - - out.append(" icon: "); - out.append("'").append(this->icon).append("'"); - out.append("\n"); - - out.append(" assumed_state: "); - out.append(YESNO(this->assumed_state)); - out.append("\n"); - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - - out.append(" device_class: "); - out.append("'").append(this->device_class).append("'"); - out.append("\n"); - out.append("}"); -} -#endif bool SwitchStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { this->state = value.as_bool(); return true; } + case 3: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -3004,26 +1624,13 @@ bool SwitchStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { void SwitchStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->state); + buffer.encode_uint32(3, this->device_id); } void SwitchStateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); ProtoSize::add_bool_field(total_size, 1, this->state, false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void SwitchStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("SwitchStateResponse {\n"); - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - out.append(YESNO(this->state)); - out.append("\n"); - out.append("}"); -} -#endif bool SwitchCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -3052,21 +1659,8 @@ void SwitchCommandRequest::calculate_size(uint32_t &total_size) const { ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); ProtoSize::add_bool_field(total_size, 1, this->state, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void SwitchCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("SwitchCommandRequest {\n"); - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - out.append(YESNO(this->state)); - out.append("\n"); - out.append("}"); -} #endif +#ifdef USE_TEXT_SENSOR bool ListEntitiesTextSensorResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 6: { @@ -3077,6 +1671,10 @@ bool ListEntitiesTextSensorResponse::decode_varint(uint32_t field_id, ProtoVarIn this->entity_category = value.as_enum(); return true; } + case 9: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -3126,6 +1724,7 @@ void ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(6, this->disabled_by_default); buffer.encode_enum(7, this->entity_category); buffer.encode_string(8, this->device_class); + buffer.encode_uint32(9, this->device_id); } void ListEntitiesTextSensorResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -3136,52 +1735,18 @@ void ListEntitiesTextSensorResponse::calculate_size(uint32_t &total_size) const ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); ProtoSize::add_string_field(total_size, 1, this->device_class, false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void ListEntitiesTextSensorResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesTextSensorResponse {\n"); - out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); - out.append("\n"); - - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - out.append("'").append(this->name).append("'"); - out.append("\n"); - - out.append(" unique_id: "); - out.append("'").append(this->unique_id).append("'"); - out.append("\n"); - - out.append(" icon: "); - out.append("'").append(this->icon).append("'"); - out.append("\n"); - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - - out.append(" device_class: "); - out.append("'").append(this->device_class).append("'"); - out.append("\n"); - out.append("}"); -} -#endif bool TextSensorStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 3: { this->missing_state = value.as_bool(); return true; } + case 4: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -3210,29 +1775,13 @@ void TextSensorStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_string(2, this->state); buffer.encode_bool(3, this->missing_state); + buffer.encode_uint32(4, this->device_id); } void TextSensorStateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); ProtoSize::add_string_field(total_size, 1, this->state, false); ProtoSize::add_bool_field(total_size, 1, this->missing_state, false); -} -#ifdef HAS_PROTO_MESSAGE_DUMP -void TextSensorStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("TextSensorStateResponse {\n"); - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - out.append("'").append(this->state).append("'"); - out.append("\n"); - - out.append(" missing_state: "); - out.append(YESNO(this->missing_state)); - out.append("\n"); - out.append("}"); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } #endif bool SubscribeLogsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { @@ -3257,20 +1806,6 @@ void SubscribeLogsRequest::calculate_size(uint32_t &total_size) const { ProtoSize::add_enum_field(total_size, 1, static_cast(this->level), false); ProtoSize::add_bool_field(total_size, 1, this->dump_config, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void SubscribeLogsRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("SubscribeLogsRequest {\n"); - out.append(" level: "); - out.append(proto_enum_to_string(this->level)); - out.append("\n"); - - out.append(" dump_config: "); - out.append(YESNO(this->dump_config)); - out.append("\n"); - out.append("}"); -} -#endif bool SubscribeLogsResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -3297,7 +1832,7 @@ bool SubscribeLogsResponse::decode_length(uint32_t field_id, ProtoLengthDelimite } void SubscribeLogsResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_enum(1, this->level); - buffer.encode_string(3, this->message); + buffer.encode_bytes(3, reinterpret_cast(this->message.data()), this->message.size()); buffer.encode_bool(4, this->send_failed); } void SubscribeLogsResponse::calculate_size(uint32_t &total_size) const { @@ -3305,24 +1840,7 @@ void SubscribeLogsResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->message, false); ProtoSize::add_bool_field(total_size, 1, this->send_failed, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void SubscribeLogsResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("SubscribeLogsResponse {\n"); - out.append(" level: "); - out.append(proto_enum_to_string(this->level)); - out.append("\n"); - - out.append(" message: "); - out.append("'").append(this->message).append("'"); - out.append("\n"); - - out.append(" send_failed: "); - out.append(YESNO(this->send_failed)); - out.append("\n"); - out.append("}"); -} -#endif +#ifdef USE_API_NOISE bool NoiseEncryptionSetKeyRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 1: { @@ -3333,20 +1851,12 @@ bool NoiseEncryptionSetKeyRequest::decode_length(uint32_t field_id, ProtoLengthD return false; } } -void NoiseEncryptionSetKeyRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->key); } +void NoiseEncryptionSetKeyRequest::encode(ProtoWriteBuffer buffer) const { + buffer.encode_bytes(1, reinterpret_cast(this->key.data()), this->key.size()); +} void NoiseEncryptionSetKeyRequest::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->key, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void NoiseEncryptionSetKeyRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("NoiseEncryptionSetKeyRequest {\n"); - out.append(" key: "); - out.append("'").append(this->key).append("'"); - out.append("\n"); - out.append("}"); -} -#endif bool NoiseEncryptionSetKeyResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -3361,22 +1871,6 @@ void NoiseEncryptionSetKeyResponse::encode(ProtoWriteBuffer buffer) const { buff void NoiseEncryptionSetKeyResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 1, this->success, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void NoiseEncryptionSetKeyResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("NoiseEncryptionSetKeyResponse {\n"); - out.append(" success: "); - out.append(YESNO(this->success)); - out.append("\n"); - out.append("}"); -} -#endif -void SubscribeHomeassistantServicesRequest::encode(ProtoWriteBuffer buffer) const {} -void SubscribeHomeassistantServicesRequest::calculate_size(uint32_t &total_size) const {} -#ifdef HAS_PROTO_MESSAGE_DUMP -void SubscribeHomeassistantServicesRequest::dump_to(std::string &out) const { - out.append("SubscribeHomeassistantServicesRequest {}"); -} #endif bool HomeassistantServiceMap::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { @@ -3400,20 +1894,6 @@ void HomeassistantServiceMap::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->key, false); ProtoSize::add_string_field(total_size, 1, this->value, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void HomeassistantServiceMap::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("HomeassistantServiceMap {\n"); - out.append(" key: "); - out.append("'").append(this->key).append("'"); - out.append("\n"); - - out.append(" value: "); - out.append("'").append(this->value).append("'"); - out.append("\n"); - out.append("}"); -} -#endif bool HomeassistantServiceResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 5: { @@ -3466,45 +1946,6 @@ void HomeassistantServiceResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_repeated_message(total_size, 1, this->variables); ProtoSize::add_bool_field(total_size, 1, this->is_event, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void HomeassistantServiceResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("HomeassistantServiceResponse {\n"); - out.append(" service: "); - out.append("'").append(this->service).append("'"); - out.append("\n"); - - for (const auto &it : this->data) { - out.append(" data: "); - it.dump_to(out); - out.append("\n"); - } - - for (const auto &it : this->data_template) { - out.append(" data_template: "); - it.dump_to(out); - out.append("\n"); - } - - for (const auto &it : this->variables) { - out.append(" variables: "); - it.dump_to(out); - out.append("\n"); - } - - out.append(" is_event: "); - out.append(YESNO(this->is_event)); - out.append("\n"); - out.append("}"); -} -#endif -void SubscribeHomeAssistantStatesRequest::encode(ProtoWriteBuffer buffer) const {} -void SubscribeHomeAssistantStatesRequest::calculate_size(uint32_t &total_size) const {} -#ifdef HAS_PROTO_MESSAGE_DUMP -void SubscribeHomeAssistantStatesRequest::dump_to(std::string &out) const { - out.append("SubscribeHomeAssistantStatesRequest {}"); -} -#endif bool SubscribeHomeAssistantStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 3: { @@ -3539,24 +1980,6 @@ void SubscribeHomeAssistantStateResponse::calculate_size(uint32_t &total_size) c ProtoSize::add_string_field(total_size, 1, this->attribute, false); ProtoSize::add_bool_field(total_size, 1, this->once, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void SubscribeHomeAssistantStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("SubscribeHomeAssistantStateResponse {\n"); - out.append(" entity_id: "); - out.append("'").append(this->entity_id).append("'"); - out.append("\n"); - - out.append(" attribute: "); - out.append("'").append(this->attribute).append("'"); - out.append("\n"); - - out.append(" once: "); - out.append(YESNO(this->once)); - out.append("\n"); - out.append("}"); -} -#endif bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 1: { @@ -3585,29 +2008,6 @@ void HomeAssistantStateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->state, false); ProtoSize::add_string_field(total_size, 1, this->attribute, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void HomeAssistantStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("HomeAssistantStateResponse {\n"); - out.append(" entity_id: "); - out.append("'").append(this->entity_id).append("'"); - out.append("\n"); - - out.append(" state: "); - out.append("'").append(this->state).append("'"); - out.append("\n"); - - out.append(" attribute: "); - out.append("'").append(this->attribute).append("'"); - out.append("\n"); - out.append("}"); -} -#endif -void GetTimeRequest::encode(ProtoWriteBuffer buffer) const {} -void GetTimeRequest::calculate_size(uint32_t &total_size) const {} -#ifdef HAS_PROTO_MESSAGE_DUMP -void GetTimeRequest::dump_to(std::string &out) const { out.append("GetTimeRequest {}"); } -#endif bool GetTimeResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { case 1: { @@ -3622,17 +2022,6 @@ void GetTimeResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixe void GetTimeResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_fixed_field<4>(total_size, 1, this->epoch_seconds != 0, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void GetTimeResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("GetTimeResponse {\n"); - out.append(" epoch_seconds: "); - sprintf(buffer, "%" PRIu32, this->epoch_seconds); - out.append(buffer); - out.append("\n"); - out.append("}"); -} -#endif bool ListEntitiesServicesArgument::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -3661,20 +2050,6 @@ void ListEntitiesServicesArgument::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->name, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->type), false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void ListEntitiesServicesArgument::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesServicesArgument {\n"); - out.append(" name: "); - out.append("'").append(this->name).append("'"); - out.append("\n"); - - out.append(" type: "); - out.append(proto_enum_to_string(this->type)); - out.append("\n"); - out.append("}"); -} -#endif bool ListEntitiesServicesResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 1: { @@ -3711,27 +2086,6 @@ void ListEntitiesServicesResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); ProtoSize::add_repeated_message(total_size, 1, this->args); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void ListEntitiesServicesResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesServicesResponse {\n"); - out.append(" name: "); - out.append("'").append(this->name).append("'"); - out.append("\n"); - - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - for (const auto &it : this->args) { - out.append(" args: "); - it.dump_to(out); - out.append("\n"); - } - out.append("}"); -} -#endif bool ExecuteServiceArgument::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -3832,61 +2186,6 @@ void ExecuteServiceArgument::calculate_size(uint32_t &total_size) const { } } } -#ifdef HAS_PROTO_MESSAGE_DUMP -void ExecuteServiceArgument::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ExecuteServiceArgument {\n"); - out.append(" bool_: "); - out.append(YESNO(this->bool_)); - out.append("\n"); - - out.append(" legacy_int: "); - sprintf(buffer, "%" PRId32, this->legacy_int); - out.append(buffer); - out.append("\n"); - - out.append(" float_: "); - sprintf(buffer, "%g", this->float_); - out.append(buffer); - out.append("\n"); - - out.append(" string_: "); - out.append("'").append(this->string_).append("'"); - out.append("\n"); - - out.append(" int_: "); - sprintf(buffer, "%" PRId32, this->int_); - out.append(buffer); - out.append("\n"); - - for (const auto it : this->bool_array) { - out.append(" bool_array: "); - out.append(YESNO(it)); - out.append("\n"); - } - - for (const auto &it : this->int_array) { - out.append(" int_array: "); - sprintf(buffer, "%" PRId32, it); - out.append(buffer); - out.append("\n"); - } - - for (const auto &it : this->float_array) { - out.append(" float_array: "); - sprintf(buffer, "%g", it); - out.append(buffer); - out.append("\n"); - } - - for (const auto &it : this->string_array) { - out.append(" string_array: "); - out.append("'").append(it).append("'"); - out.append("\n"); - } - out.append("}"); -} -#endif bool ExecuteServiceRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 2: { @@ -3917,23 +2216,7 @@ void ExecuteServiceRequest::calculate_size(uint32_t &total_size) const { ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); ProtoSize::add_repeated_message(total_size, 1, this->args); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void ExecuteServiceRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ExecuteServiceRequest {\n"); - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - for (const auto &it : this->args) { - out.append(" args: "); - it.dump_to(out); - out.append("\n"); - } - out.append("}"); -} -#endif +#ifdef USE_CAMERA bool ListEntitiesCameraResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 5: { @@ -3944,6 +2227,10 @@ bool ListEntitiesCameraResponse::decode_varint(uint32_t field_id, ProtoVarInt va this->entity_category = value.as_enum(); return true; } + case 8: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -3988,6 +2275,7 @@ void ListEntitiesCameraResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(5, this->disabled_by_default); buffer.encode_string(6, this->icon); buffer.encode_enum(7, this->entity_category); + buffer.encode_uint32(8, this->device_id); } void ListEntitiesCameraResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -3997,42 +2285,8 @@ void ListEntitiesCameraResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); ProtoSize::add_string_field(total_size, 1, this->icon, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void ListEntitiesCameraResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesCameraResponse {\n"); - out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); - out.append("\n"); - - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - out.append("'").append(this->name).append("'"); - out.append("\n"); - - out.append(" unique_id: "); - out.append("'").append(this->unique_id).append("'"); - out.append("\n"); - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" icon: "); - out.append("'").append(this->icon).append("'"); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - out.append("}"); -} -#endif bool CameraImageResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 3: { @@ -4065,7 +2319,7 @@ bool CameraImageResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { } void CameraImageResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); - buffer.encode_string(2, this->data); + buffer.encode_bytes(2, reinterpret_cast(this->data.data()), this->data.size()); buffer.encode_bool(3, this->done); } void CameraImageResponse::calculate_size(uint32_t &total_size) const { @@ -4073,25 +2327,6 @@ void CameraImageResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->data, false); ProtoSize::add_bool_field(total_size, 1, this->done, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void CameraImageResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("CameraImageResponse {\n"); - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" data: "); - out.append("'").append(this->data).append("'"); - out.append("\n"); - - out.append(" done: "); - out.append(YESNO(this->done)); - out.append("\n"); - out.append("}"); -} -#endif bool CameraImageRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -4114,20 +2349,8 @@ void CameraImageRequest::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 1, this->single, false); ProtoSize::add_bool_field(total_size, 1, this->stream, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void CameraImageRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("CameraImageRequest {\n"); - out.append(" single: "); - out.append(YESNO(this->single)); - out.append("\n"); - - out.append(" stream: "); - out.append(YESNO(this->stream)); - out.append("\n"); - out.append("}"); -} #endif +#ifdef USE_CLIMATE bool ListEntitiesClimateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 5: { @@ -4178,6 +2401,10 @@ bool ListEntitiesClimateResponse::decode_varint(uint32_t field_id, ProtoVarInt v this->supports_target_humidity = value.as_bool(); return true; } + case 26: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -4284,6 +2511,7 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(23, this->supports_target_humidity); buffer.encode_float(24, this->visual_min_humidity); buffer.encode_float(25, this->visual_max_humidity); + buffer.encode_uint32(26, this->device_id); } void ListEntitiesClimateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -4335,132 +2563,8 @@ void ListEntitiesClimateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 2, this->supports_target_humidity, false); ProtoSize::add_fixed_field<4>(total_size, 2, this->visual_min_humidity != 0.0f, false); ProtoSize::add_fixed_field<4>(total_size, 2, this->visual_max_humidity != 0.0f, false); + ProtoSize::add_uint32_field(total_size, 2, this->device_id, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void ListEntitiesClimateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesClimateResponse {\n"); - out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); - out.append("\n"); - - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - out.append("'").append(this->name).append("'"); - out.append("\n"); - - out.append(" unique_id: "); - out.append("'").append(this->unique_id).append("'"); - out.append("\n"); - - out.append(" supports_current_temperature: "); - out.append(YESNO(this->supports_current_temperature)); - out.append("\n"); - - out.append(" supports_two_point_target_temperature: "); - out.append(YESNO(this->supports_two_point_target_temperature)); - out.append("\n"); - - for (const auto &it : this->supported_modes) { - out.append(" supported_modes: "); - out.append(proto_enum_to_string(it)); - out.append("\n"); - } - - out.append(" visual_min_temperature: "); - sprintf(buffer, "%g", this->visual_min_temperature); - out.append(buffer); - out.append("\n"); - - out.append(" visual_max_temperature: "); - sprintf(buffer, "%g", this->visual_max_temperature); - out.append(buffer); - out.append("\n"); - - out.append(" visual_target_temperature_step: "); - sprintf(buffer, "%g", this->visual_target_temperature_step); - out.append(buffer); - out.append("\n"); - - out.append(" legacy_supports_away: "); - out.append(YESNO(this->legacy_supports_away)); - out.append("\n"); - - out.append(" supports_action: "); - out.append(YESNO(this->supports_action)); - out.append("\n"); - - for (const auto &it : this->supported_fan_modes) { - out.append(" supported_fan_modes: "); - out.append(proto_enum_to_string(it)); - out.append("\n"); - } - - for (const auto &it : this->supported_swing_modes) { - out.append(" supported_swing_modes: "); - out.append(proto_enum_to_string(it)); - out.append("\n"); - } - - for (const auto &it : this->supported_custom_fan_modes) { - out.append(" supported_custom_fan_modes: "); - out.append("'").append(it).append("'"); - out.append("\n"); - } - - for (const auto &it : this->supported_presets) { - out.append(" supported_presets: "); - out.append(proto_enum_to_string(it)); - out.append("\n"); - } - - for (const auto &it : this->supported_custom_presets) { - out.append(" supported_custom_presets: "); - out.append("'").append(it).append("'"); - out.append("\n"); - } - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" icon: "); - out.append("'").append(this->icon).append("'"); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - - out.append(" visual_current_temperature_step: "); - sprintf(buffer, "%g", this->visual_current_temperature_step); - out.append(buffer); - out.append("\n"); - - out.append(" supports_current_humidity: "); - out.append(YESNO(this->supports_current_humidity)); - out.append("\n"); - - out.append(" supports_target_humidity: "); - out.append(YESNO(this->supports_target_humidity)); - out.append("\n"); - - out.append(" visual_min_humidity: "); - sprintf(buffer, "%g", this->visual_min_humidity); - out.append(buffer); - out.append("\n"); - - out.append(" visual_max_humidity: "); - sprintf(buffer, "%g", this->visual_max_humidity); - out.append(buffer); - out.append("\n"); - out.append("}"); -} -#endif bool ClimateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -4487,6 +2591,10 @@ bool ClimateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { this->preset = value.as_enum(); return true; } + case 16: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -4555,6 +2663,7 @@ void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(13, this->custom_preset); buffer.encode_float(14, this->current_humidity); buffer.encode_float(15, this->target_humidity); + buffer.encode_uint32(16, this->device_id); } void ClimateStateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); @@ -4572,80 +2681,8 @@ void ClimateStateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->custom_preset, false); ProtoSize::add_fixed_field<4>(total_size, 1, this->current_humidity != 0.0f, false); ProtoSize::add_fixed_field<4>(total_size, 1, this->target_humidity != 0.0f, false); + ProtoSize::add_uint32_field(total_size, 2, this->device_id, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void ClimateStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ClimateStateResponse {\n"); - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" mode: "); - out.append(proto_enum_to_string(this->mode)); - out.append("\n"); - - out.append(" current_temperature: "); - sprintf(buffer, "%g", this->current_temperature); - out.append(buffer); - out.append("\n"); - - out.append(" target_temperature: "); - sprintf(buffer, "%g", this->target_temperature); - out.append(buffer); - out.append("\n"); - - out.append(" target_temperature_low: "); - sprintf(buffer, "%g", this->target_temperature_low); - out.append(buffer); - out.append("\n"); - - out.append(" target_temperature_high: "); - sprintf(buffer, "%g", this->target_temperature_high); - out.append(buffer); - out.append("\n"); - - out.append(" unused_legacy_away: "); - out.append(YESNO(this->unused_legacy_away)); - out.append("\n"); - - out.append(" action: "); - out.append(proto_enum_to_string(this->action)); - out.append("\n"); - - out.append(" fan_mode: "); - out.append(proto_enum_to_string(this->fan_mode)); - out.append("\n"); - - out.append(" swing_mode: "); - out.append(proto_enum_to_string(this->swing_mode)); - out.append("\n"); - - out.append(" custom_fan_mode: "); - out.append("'").append(this->custom_fan_mode).append("'"); - out.append("\n"); - - out.append(" preset: "); - out.append(proto_enum_to_string(this->preset)); - out.append("\n"); - - out.append(" custom_preset: "); - out.append("'").append(this->custom_preset).append("'"); - out.append("\n"); - - out.append(" current_humidity: "); - sprintf(buffer, "%g", this->current_humidity); - out.append(buffer); - out.append("\n"); - - out.append(" target_humidity: "); - sprintf(buffer, "%g", this->target_humidity); - out.append(buffer); - out.append("\n"); - out.append("}"); -} -#endif bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -4806,109 +2843,8 @@ void ClimateCommandRequest::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 2, this->has_target_humidity, false); ProtoSize::add_fixed_field<4>(total_size, 2, this->target_humidity != 0.0f, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void ClimateCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ClimateCommandRequest {\n"); - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" has_mode: "); - out.append(YESNO(this->has_mode)); - out.append("\n"); - - out.append(" mode: "); - out.append(proto_enum_to_string(this->mode)); - out.append("\n"); - - out.append(" has_target_temperature: "); - out.append(YESNO(this->has_target_temperature)); - out.append("\n"); - - out.append(" target_temperature: "); - sprintf(buffer, "%g", this->target_temperature); - out.append(buffer); - out.append("\n"); - - out.append(" has_target_temperature_low: "); - out.append(YESNO(this->has_target_temperature_low)); - out.append("\n"); - - out.append(" target_temperature_low: "); - sprintf(buffer, "%g", this->target_temperature_low); - out.append(buffer); - out.append("\n"); - - out.append(" has_target_temperature_high: "); - out.append(YESNO(this->has_target_temperature_high)); - out.append("\n"); - - out.append(" target_temperature_high: "); - sprintf(buffer, "%g", this->target_temperature_high); - out.append(buffer); - out.append("\n"); - - out.append(" unused_has_legacy_away: "); - out.append(YESNO(this->unused_has_legacy_away)); - out.append("\n"); - - out.append(" unused_legacy_away: "); - out.append(YESNO(this->unused_legacy_away)); - out.append("\n"); - - out.append(" has_fan_mode: "); - out.append(YESNO(this->has_fan_mode)); - out.append("\n"); - - out.append(" fan_mode: "); - out.append(proto_enum_to_string(this->fan_mode)); - out.append("\n"); - - out.append(" has_swing_mode: "); - out.append(YESNO(this->has_swing_mode)); - out.append("\n"); - - out.append(" swing_mode: "); - out.append(proto_enum_to_string(this->swing_mode)); - out.append("\n"); - - out.append(" has_custom_fan_mode: "); - out.append(YESNO(this->has_custom_fan_mode)); - out.append("\n"); - - out.append(" custom_fan_mode: "); - out.append("'").append(this->custom_fan_mode).append("'"); - out.append("\n"); - - out.append(" has_preset: "); - out.append(YESNO(this->has_preset)); - out.append("\n"); - - out.append(" preset: "); - out.append(proto_enum_to_string(this->preset)); - out.append("\n"); - - out.append(" has_custom_preset: "); - out.append(YESNO(this->has_custom_preset)); - out.append("\n"); - - out.append(" custom_preset: "); - out.append("'").append(this->custom_preset).append("'"); - out.append("\n"); - - out.append(" has_target_humidity: "); - out.append(YESNO(this->has_target_humidity)); - out.append("\n"); - - out.append(" target_humidity: "); - sprintf(buffer, "%g", this->target_humidity); - out.append(buffer); - out.append("\n"); - out.append("}"); -} #endif +#ifdef USE_NUMBER bool ListEntitiesNumberResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 9: { @@ -4923,6 +2859,10 @@ bool ListEntitiesNumberResponse::decode_varint(uint32_t field_id, ProtoVarInt va this->mode = value.as_enum(); return true; } + case 14: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -4993,6 +2933,7 @@ void ListEntitiesNumberResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(11, this->unit_of_measurement); buffer.encode_enum(12, this->mode); buffer.encode_string(13, this->device_class); + buffer.encode_uint32(14, this->device_id); } void ListEntitiesNumberResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -5008,75 +2949,18 @@ void ListEntitiesNumberResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->unit_of_measurement, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->mode), false); ProtoSize::add_string_field(total_size, 1, this->device_class, false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void ListEntitiesNumberResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesNumberResponse {\n"); - out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); - out.append("\n"); - - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - out.append("'").append(this->name).append("'"); - out.append("\n"); - - out.append(" unique_id: "); - out.append("'").append(this->unique_id).append("'"); - out.append("\n"); - - out.append(" icon: "); - out.append("'").append(this->icon).append("'"); - out.append("\n"); - - out.append(" min_value: "); - sprintf(buffer, "%g", this->min_value); - out.append(buffer); - out.append("\n"); - - out.append(" max_value: "); - sprintf(buffer, "%g", this->max_value); - out.append(buffer); - out.append("\n"); - - out.append(" step: "); - sprintf(buffer, "%g", this->step); - out.append(buffer); - out.append("\n"); - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - - out.append(" unit_of_measurement: "); - out.append("'").append(this->unit_of_measurement).append("'"); - out.append("\n"); - - out.append(" mode: "); - out.append(proto_enum_to_string(this->mode)); - out.append("\n"); - - out.append(" device_class: "); - out.append("'").append(this->device_class).append("'"); - out.append("\n"); - out.append("}"); -} -#endif bool NumberStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 3: { this->missing_state = value.as_bool(); return true; } + case 4: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -5099,32 +2983,14 @@ void NumberStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_float(2, this->state); buffer.encode_bool(3, this->missing_state); + buffer.encode_uint32(4, this->device_id); } void NumberStateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); ProtoSize::add_fixed_field<4>(total_size, 1, this->state != 0.0f, false); ProtoSize::add_bool_field(total_size, 1, this->missing_state, false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void NumberStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("NumberStateResponse {\n"); - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - sprintf(buffer, "%g", this->state); - out.append(buffer); - out.append("\n"); - - out.append(" missing_state: "); - out.append(YESNO(this->missing_state)); - out.append("\n"); - out.append("}"); -} -#endif bool NumberCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { case 1: { @@ -5147,22 +3013,8 @@ void NumberCommandRequest::calculate_size(uint32_t &total_size) const { ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); ProtoSize::add_fixed_field<4>(total_size, 1, this->state != 0.0f, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void NumberCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("NumberCommandRequest {\n"); - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - sprintf(buffer, "%g", this->state); - out.append(buffer); - out.append("\n"); - out.append("}"); -} #endif +#ifdef USE_SELECT bool ListEntitiesSelectResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 7: { @@ -5173,6 +3025,10 @@ bool ListEntitiesSelectResponse::decode_varint(uint32_t field_id, ProtoVarInt va this->entity_category = value.as_enum(); return true; } + case 9: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -5224,6 +3080,7 @@ void ListEntitiesSelectResponse::encode(ProtoWriteBuffer buffer) const { } buffer.encode_bool(7, this->disabled_by_default); buffer.encode_enum(8, this->entity_category); + buffer.encode_uint32(9, this->device_id); } void ListEntitiesSelectResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -5238,54 +3095,18 @@ void ListEntitiesSelectResponse::calculate_size(uint32_t &total_size) const { } ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void ListEntitiesSelectResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesSelectResponse {\n"); - out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); - out.append("\n"); - - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - out.append("'").append(this->name).append("'"); - out.append("\n"); - - out.append(" unique_id: "); - out.append("'").append(this->unique_id).append("'"); - out.append("\n"); - - out.append(" icon: "); - out.append("'").append(this->icon).append("'"); - out.append("\n"); - - for (const auto &it : this->options) { - out.append(" options: "); - out.append("'").append(it).append("'"); - out.append("\n"); - } - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - out.append("}"); -} -#endif bool SelectStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 3: { this->missing_state = value.as_bool(); return true; } + case 4: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -5314,31 +3135,14 @@ void SelectStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_string(2, this->state); buffer.encode_bool(3, this->missing_state); + buffer.encode_uint32(4, this->device_id); } void SelectStateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); ProtoSize::add_string_field(total_size, 1, this->state, false); ProtoSize::add_bool_field(total_size, 1, this->missing_state, false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void SelectStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("SelectStateResponse {\n"); - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - out.append("'").append(this->state).append("'"); - out.append("\n"); - - out.append(" missing_state: "); - out.append(YESNO(this->missing_state)); - out.append("\n"); - out.append("}"); -} -#endif bool SelectCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 2: { @@ -5367,21 +3171,8 @@ void SelectCommandRequest::calculate_size(uint32_t &total_size) const { ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); ProtoSize::add_string_field(total_size, 1, this->state, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void SelectCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("SelectCommandRequest {\n"); - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - out.append("'").append(this->state).append("'"); - out.append("\n"); - out.append("}"); -} #endif +#ifdef USE_SIREN bool ListEntitiesSirenResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 6: { @@ -5400,6 +3191,10 @@ bool ListEntitiesSirenResponse::decode_varint(uint32_t field_id, ProtoVarInt val this->entity_category = value.as_enum(); return true; } + case 11: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -5453,6 +3248,7 @@ void ListEntitiesSirenResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(8, this->supports_duration); buffer.encode_bool(9, this->supports_volume); buffer.encode_enum(10, this->entity_category); + buffer.encode_uint32(11, this->device_id); } void ListEntitiesSirenResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -5469,62 +3265,18 @@ void ListEntitiesSirenResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 1, this->supports_duration, false); ProtoSize::add_bool_field(total_size, 1, this->supports_volume, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void ListEntitiesSirenResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesSirenResponse {\n"); - out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); - out.append("\n"); - - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - out.append("'").append(this->name).append("'"); - out.append("\n"); - - out.append(" unique_id: "); - out.append("'").append(this->unique_id).append("'"); - out.append("\n"); - - out.append(" icon: "); - out.append("'").append(this->icon).append("'"); - out.append("\n"); - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - for (const auto &it : this->tones) { - out.append(" tones: "); - out.append("'").append(it).append("'"); - out.append("\n"); - } - - out.append(" supports_duration: "); - out.append(YESNO(this->supports_duration)); - out.append("\n"); - - out.append(" supports_volume: "); - out.append(YESNO(this->supports_volume)); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - out.append("}"); -} -#endif bool SirenStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { this->state = value.as_bool(); return true; } + case 3: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -5542,26 +3294,13 @@ bool SirenStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { void SirenStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->state); + buffer.encode_uint32(3, this->device_id); } void SirenStateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); ProtoSize::add_bool_field(total_size, 1, this->state, false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void SirenStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("SirenStateResponse {\n"); - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - out.append(YESNO(this->state)); - out.append("\n"); - out.append("}"); -} -#endif bool SirenCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -5638,51 +3377,8 @@ void SirenCommandRequest::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 1, this->has_volume, false); ProtoSize::add_fixed_field<4>(total_size, 1, this->volume != 0.0f, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void SirenCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("SirenCommandRequest {\n"); - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" has_state: "); - out.append(YESNO(this->has_state)); - out.append("\n"); - - out.append(" state: "); - out.append(YESNO(this->state)); - out.append("\n"); - - out.append(" has_tone: "); - out.append(YESNO(this->has_tone)); - out.append("\n"); - - out.append(" tone: "); - out.append("'").append(this->tone).append("'"); - out.append("\n"); - - out.append(" has_duration: "); - out.append(YESNO(this->has_duration)); - out.append("\n"); - - out.append(" duration: "); - sprintf(buffer, "%" PRIu32, this->duration); - out.append(buffer); - out.append("\n"); - - out.append(" has_volume: "); - out.append(YESNO(this->has_volume)); - out.append("\n"); - - out.append(" volume: "); - sprintf(buffer, "%g", this->volume); - out.append(buffer); - out.append("\n"); - out.append("}"); -} #endif +#ifdef USE_LOCK bool ListEntitiesLockResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 6: { @@ -5705,6 +3401,10 @@ bool ListEntitiesLockResponse::decode_varint(uint32_t field_id, ProtoVarInt valu this->requires_code = value.as_bool(); return true; } + case 12: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -5757,6 +3457,7 @@ void ListEntitiesLockResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(9, this->supports_open); buffer.encode_bool(10, this->requires_code); buffer.encode_string(11, this->code_format); + buffer.encode_uint32(12, this->device_id); } void ListEntitiesLockResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -5770,64 +3471,18 @@ void ListEntitiesLockResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 1, this->supports_open, false); ProtoSize::add_bool_field(total_size, 1, this->requires_code, false); ProtoSize::add_string_field(total_size, 1, this->code_format, false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void ListEntitiesLockResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesLockResponse {\n"); - out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); - out.append("\n"); - - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - out.append("'").append(this->name).append("'"); - out.append("\n"); - - out.append(" unique_id: "); - out.append("'").append(this->unique_id).append("'"); - out.append("\n"); - - out.append(" icon: "); - out.append("'").append(this->icon).append("'"); - out.append("\n"); - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - - out.append(" assumed_state: "); - out.append(YESNO(this->assumed_state)); - out.append("\n"); - - out.append(" supports_open: "); - out.append(YESNO(this->supports_open)); - out.append("\n"); - - out.append(" requires_code: "); - out.append(YESNO(this->requires_code)); - out.append("\n"); - - out.append(" code_format: "); - out.append("'").append(this->code_format).append("'"); - out.append("\n"); - out.append("}"); -} -#endif bool LockStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { this->state = value.as_enum(); return true; } + case 3: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -5845,26 +3500,13 @@ bool LockStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { void LockStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_enum(2, this->state); + buffer.encode_uint32(3, this->device_id); } void LockStateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->state), false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void LockStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("LockStateResponse {\n"); - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - out.append(proto_enum_to_string(this->state)); - out.append("\n"); - out.append("}"); -} -#endif bool LockCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -5911,29 +3553,8 @@ void LockCommandRequest::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 1, this->has_code, false); ProtoSize::add_string_field(total_size, 1, this->code, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void LockCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("LockCommandRequest {\n"); - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" command: "); - out.append(proto_enum_to_string(this->command)); - out.append("\n"); - - out.append(" has_code: "); - out.append(YESNO(this->has_code)); - out.append("\n"); - - out.append(" code: "); - out.append("'").append(this->code).append("'"); - out.append("\n"); - out.append("}"); -} #endif +#ifdef USE_BUTTON bool ListEntitiesButtonResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 6: { @@ -5944,6 +3565,10 @@ bool ListEntitiesButtonResponse::decode_varint(uint32_t field_id, ProtoVarInt va this->entity_category = value.as_enum(); return true; } + case 9: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -5993,6 +3618,7 @@ void ListEntitiesButtonResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(6, this->disabled_by_default); buffer.encode_enum(7, this->entity_category); buffer.encode_string(8, this->device_class); + buffer.encode_uint32(9, this->device_id); } void ListEntitiesButtonResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -6003,46 +3629,8 @@ void ListEntitiesButtonResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); ProtoSize::add_string_field(total_size, 1, this->device_class, false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void ListEntitiesButtonResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesButtonResponse {\n"); - out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); - out.append("\n"); - - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - out.append("'").append(this->name).append("'"); - out.append("\n"); - - out.append(" unique_id: "); - out.append("'").append(this->unique_id).append("'"); - out.append("\n"); - - out.append(" icon: "); - out.append("'").append(this->icon).append("'"); - out.append("\n"); - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - - out.append(" device_class: "); - out.append("'").append(this->device_class).append("'"); - out.append("\n"); - out.append("}"); -} -#endif bool ButtonCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { case 1: { @@ -6057,17 +3645,8 @@ void ButtonCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode void ButtonCommandRequest::calculate_size(uint32_t &total_size) const { ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void ButtonCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ButtonCommandRequest {\n"); - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - out.append("}"); -} #endif +#ifdef USE_MEDIA_PLAYER bool MediaPlayerSupportedFormat::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -6114,35 +3693,6 @@ void MediaPlayerSupportedFormat::calculate_size(uint32_t &total_size) const { ProtoSize::add_enum_field(total_size, 1, static_cast(this->purpose), false); ProtoSize::add_uint32_field(total_size, 1, this->sample_bytes, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void MediaPlayerSupportedFormat::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("MediaPlayerSupportedFormat {\n"); - out.append(" format: "); - out.append("'").append(this->format).append("'"); - out.append("\n"); - - out.append(" sample_rate: "); - sprintf(buffer, "%" PRIu32, this->sample_rate); - out.append(buffer); - out.append("\n"); - - out.append(" num_channels: "); - sprintf(buffer, "%" PRIu32, this->num_channels); - out.append(buffer); - out.append("\n"); - - out.append(" purpose: "); - out.append(proto_enum_to_string(this->purpose)); - out.append("\n"); - - out.append(" sample_bytes: "); - sprintf(buffer, "%" PRIu32, this->sample_bytes); - out.append(buffer); - out.append("\n"); - out.append("}"); -} -#endif bool ListEntitiesMediaPlayerResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 6: { @@ -6157,6 +3707,10 @@ bool ListEntitiesMediaPlayerResponse::decode_varint(uint32_t field_id, ProtoVarI this->supports_pause = value.as_bool(); return true; } + case 10: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -6209,6 +3763,7 @@ void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer buffer) const { for (auto &it : this->supported_formats) { buffer.encode_message(9, it, true); } + buffer.encode_uint32(10, this->device_id); } void ListEntitiesMediaPlayerResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -6220,52 +3775,8 @@ void ListEntitiesMediaPlayerResponse::calculate_size(uint32_t &total_size) const ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); ProtoSize::add_bool_field(total_size, 1, this->supports_pause, false); ProtoSize::add_repeated_message(total_size, 1, this->supported_formats); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void ListEntitiesMediaPlayerResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesMediaPlayerResponse {\n"); - out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); - out.append("\n"); - - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - out.append("'").append(this->name).append("'"); - out.append("\n"); - - out.append(" unique_id: "); - out.append("'").append(this->unique_id).append("'"); - out.append("\n"); - - out.append(" icon: "); - out.append("'").append(this->icon).append("'"); - out.append("\n"); - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - - out.append(" supports_pause: "); - out.append(YESNO(this->supports_pause)); - out.append("\n"); - - for (const auto &it : this->supported_formats) { - out.append(" supported_formats: "); - it.dump_to(out); - out.append("\n"); - } - out.append("}"); -} -#endif bool MediaPlayerStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -6276,6 +3787,10 @@ bool MediaPlayerStateResponse::decode_varint(uint32_t field_id, ProtoVarInt valu this->muted = value.as_bool(); return true; } + case 5: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -6299,37 +3814,15 @@ void MediaPlayerStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_enum(2, this->state); buffer.encode_float(3, this->volume); buffer.encode_bool(4, this->muted); + buffer.encode_uint32(5, this->device_id); } void MediaPlayerStateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->state), false); ProtoSize::add_fixed_field<4>(total_size, 1, this->volume != 0.0f, false); ProtoSize::add_bool_field(total_size, 1, this->muted, false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void MediaPlayerStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("MediaPlayerStateResponse {\n"); - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - out.append(proto_enum_to_string(this->state)); - out.append("\n"); - - out.append(" volume: "); - sprintf(buffer, "%g", this->volume); - out.append(buffer); - out.append("\n"); - - out.append(" muted: "); - out.append(YESNO(this->muted)); - out.append("\n"); - out.append("}"); -} -#endif bool MediaPlayerCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -6406,50 +3899,8 @@ void MediaPlayerCommandRequest::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 1, this->has_announcement, false); ProtoSize::add_bool_field(total_size, 1, this->announcement, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void MediaPlayerCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("MediaPlayerCommandRequest {\n"); - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" has_command: "); - out.append(YESNO(this->has_command)); - out.append("\n"); - - out.append(" command: "); - out.append(proto_enum_to_string(this->command)); - out.append("\n"); - - out.append(" has_volume: "); - out.append(YESNO(this->has_volume)); - out.append("\n"); - - out.append(" volume: "); - sprintf(buffer, "%g", this->volume); - out.append(buffer); - out.append("\n"); - - out.append(" has_media_url: "); - out.append(YESNO(this->has_media_url)); - out.append("\n"); - - out.append(" media_url: "); - out.append("'").append(this->media_url).append("'"); - out.append("\n"); - - out.append(" has_announcement: "); - out.append(YESNO(this->has_announcement)); - out.append("\n"); - - out.append(" announcement: "); - out.append(YESNO(this->announcement)); - out.append("\n"); - out.append("}"); -} #endif +#ifdef USE_BLUETOOTH_PROXY bool SubscribeBluetoothLEAdvertisementsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -6466,17 +3917,6 @@ void SubscribeBluetoothLEAdvertisementsRequest::encode(ProtoWriteBuffer buffer) void SubscribeBluetoothLEAdvertisementsRequest::calculate_size(uint32_t &total_size) const { ProtoSize::add_uint32_field(total_size, 1, this->flags, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void SubscribeBluetoothLEAdvertisementsRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("SubscribeBluetoothLEAdvertisementsRequest {\n"); - out.append(" flags: "); - sprintf(buffer, "%" PRIu32, this->flags); - out.append(buffer); - out.append("\n"); - out.append("}"); -} -#endif bool BluetoothServiceData::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -6506,7 +3946,7 @@ void BluetoothServiceData::encode(ProtoWriteBuffer buffer) const { for (auto &it : this->legacy_data) { buffer.encode_uint32(2, it, true); } - buffer.encode_string(3, this->data); + buffer.encode_bytes(3, reinterpret_cast(this->data.data()), this->data.size()); } void BluetoothServiceData::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->uuid, false); @@ -6517,27 +3957,6 @@ void BluetoothServiceData::calculate_size(uint32_t &total_size) const { } ProtoSize::add_string_field(total_size, 1, this->data, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void BluetoothServiceData::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothServiceData {\n"); - out.append(" uuid: "); - out.append("'").append(this->uuid).append("'"); - out.append("\n"); - - for (const auto &it : this->legacy_data) { - out.append(" legacy_data: "); - sprintf(buffer, "%" PRIu32, it); - out.append(buffer); - out.append("\n"); - } - - out.append(" data: "); - out.append("'").append(this->data).append("'"); - out.append("\n"); - out.append("}"); -} -#endif bool BluetoothLEAdvertisementResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -6580,7 +3999,7 @@ bool BluetoothLEAdvertisementResponse::decode_length(uint32_t field_id, ProtoLen } void BluetoothLEAdvertisementResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint64(1, this->address); - buffer.encode_string(2, this->name); + buffer.encode_bytes(2, reinterpret_cast(this->name.data()), this->name.size()); buffer.encode_sint32(3, this->rssi); for (auto &it : this->service_uuids) { buffer.encode_string(4, it, true); @@ -6606,49 +4025,6 @@ void BluetoothLEAdvertisementResponse::calculate_size(uint32_t &total_size) cons ProtoSize::add_repeated_message(total_size, 1, this->manufacturer_data); ProtoSize::add_uint32_field(total_size, 1, this->address_type, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void BluetoothLEAdvertisementResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothLEAdvertisementResponse {\n"); - out.append(" address: "); - sprintf(buffer, "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - out.append("'").append(this->name).append("'"); - out.append("\n"); - - out.append(" rssi: "); - sprintf(buffer, "%" PRId32, this->rssi); - out.append(buffer); - out.append("\n"); - - for (const auto &it : this->service_uuids) { - out.append(" service_uuids: "); - out.append("'").append(it).append("'"); - out.append("\n"); - } - - for (const auto &it : this->service_data) { - out.append(" service_data: "); - it.dump_to(out); - out.append("\n"); - } - - for (const auto &it : this->manufacturer_data) { - out.append(" manufacturer_data: "); - it.dump_to(out); - out.append("\n"); - } - - out.append(" address_type: "); - sprintf(buffer, "%" PRIu32, this->address_type); - out.append(buffer); - out.append("\n"); - out.append("}"); -} -#endif bool BluetoothLERawAdvertisement::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -6681,7 +4057,7 @@ void BluetoothLERawAdvertisement::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_sint32(2, this->rssi); buffer.encode_uint32(3, this->address_type); - buffer.encode_string(4, this->data); + buffer.encode_bytes(4, reinterpret_cast(this->data.data()), this->data.size()); } void BluetoothLERawAdvertisement::calculate_size(uint32_t &total_size) const { ProtoSize::add_uint64_field(total_size, 1, this->address, false); @@ -6689,31 +4065,6 @@ void BluetoothLERawAdvertisement::calculate_size(uint32_t &total_size) const { ProtoSize::add_uint32_field(total_size, 1, this->address_type, false); ProtoSize::add_string_field(total_size, 1, this->data, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void BluetoothLERawAdvertisement::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothLERawAdvertisement {\n"); - out.append(" address: "); - sprintf(buffer, "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" rssi: "); - sprintf(buffer, "%" PRId32, this->rssi); - out.append(buffer); - out.append("\n"); - - out.append(" address_type: "); - sprintf(buffer, "%" PRIu32, this->address_type); - out.append(buffer); - out.append("\n"); - - out.append(" data: "); - out.append("'").append(this->data).append("'"); - out.append("\n"); - out.append("}"); -} -#endif bool BluetoothLERawAdvertisementsResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 1: { @@ -6732,18 +4083,6 @@ void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer buffer) const void BluetoothLERawAdvertisementsResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_repeated_message(total_size, 1, this->advertisements); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void BluetoothLERawAdvertisementsResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothLERawAdvertisementsResponse {\n"); - for (const auto &it : this->advertisements) { - out.append(" advertisements: "); - it.dump_to(out); - out.append("\n"); - } - out.append("}"); -} -#endif bool BluetoothDeviceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -6778,30 +4117,6 @@ void BluetoothDeviceRequest::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 1, this->has_address_type, false); ProtoSize::add_uint32_field(total_size, 1, this->address_type, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void BluetoothDeviceRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothDeviceRequest {\n"); - out.append(" address: "); - sprintf(buffer, "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" request_type: "); - out.append(proto_enum_to_string(this->request_type)); - out.append("\n"); - - out.append(" has_address_type: "); - out.append(YESNO(this->has_address_type)); - out.append("\n"); - - out.append(" address_type: "); - sprintf(buffer, "%" PRIu32, this->address_type); - out.append(buffer); - out.append("\n"); - out.append("}"); -} -#endif bool BluetoothDeviceConnectionResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -6836,31 +4151,6 @@ void BluetoothDeviceConnectionResponse::calculate_size(uint32_t &total_size) con ProtoSize::add_uint32_field(total_size, 1, this->mtu, false); ProtoSize::add_int32_field(total_size, 1, this->error, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void BluetoothDeviceConnectionResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothDeviceConnectionResponse {\n"); - out.append(" address: "); - sprintf(buffer, "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" connected: "); - out.append(YESNO(this->connected)); - out.append("\n"); - - out.append(" mtu: "); - sprintf(buffer, "%" PRIu32, this->mtu); - out.append(buffer); - out.append("\n"); - - out.append(" error: "); - sprintf(buffer, "%" PRId32, this->error); - out.append(buffer); - out.append("\n"); - out.append("}"); -} -#endif bool BluetoothGATTGetServicesRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -6875,17 +4165,6 @@ void BluetoothGATTGetServicesRequest::encode(ProtoWriteBuffer buffer) const { bu void BluetoothGATTGetServicesRequest::calculate_size(uint32_t &total_size) const { ProtoSize::add_uint64_field(total_size, 1, this->address, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void BluetoothGATTGetServicesRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothGATTGetServicesRequest {\n"); - out.append(" address: "); - sprintf(buffer, "%llu", this->address); - out.append(buffer); - out.append("\n"); - out.append("}"); -} -#endif bool BluetoothGATTDescriptor::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -6914,24 +4193,6 @@ void BluetoothGATTDescriptor::calculate_size(uint32_t &total_size) const { } ProtoSize::add_uint32_field(total_size, 1, this->handle, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void BluetoothGATTDescriptor::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothGATTDescriptor {\n"); - for (const auto &it : this->uuid) { - out.append(" uuid: "); - sprintf(buffer, "%llu", it); - out.append(buffer); - out.append("\n"); - } - - out.append(" handle: "); - sprintf(buffer, "%" PRIu32, this->handle); - out.append(buffer); - out.append("\n"); - out.append("}"); -} -#endif bool BluetoothGATTCharacteristic::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -6980,35 +4241,6 @@ void BluetoothGATTCharacteristic::calculate_size(uint32_t &total_size) const { ProtoSize::add_uint32_field(total_size, 1, this->properties, false); ProtoSize::add_repeated_message(total_size, 1, this->descriptors); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void BluetoothGATTCharacteristic::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothGATTCharacteristic {\n"); - for (const auto &it : this->uuid) { - out.append(" uuid: "); - sprintf(buffer, "%llu", it); - out.append(buffer); - out.append("\n"); - } - - out.append(" handle: "); - sprintf(buffer, "%" PRIu32, this->handle); - out.append(buffer); - out.append("\n"); - - out.append(" properties: "); - sprintf(buffer, "%" PRIu32, this->properties); - out.append(buffer); - out.append("\n"); - - for (const auto &it : this->descriptors) { - out.append(" descriptors: "); - it.dump_to(out); - out.append("\n"); - } - out.append("}"); -} -#endif bool BluetoothGATTService::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -7051,30 +4283,6 @@ void BluetoothGATTService::calculate_size(uint32_t &total_size) const { ProtoSize::add_uint32_field(total_size, 1, this->handle, false); ProtoSize::add_repeated_message(total_size, 1, this->characteristics); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void BluetoothGATTService::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothGATTService {\n"); - for (const auto &it : this->uuid) { - out.append(" uuid: "); - sprintf(buffer, "%llu", it); - out.append(buffer); - out.append("\n"); - } - - out.append(" handle: "); - sprintf(buffer, "%" PRIu32, this->handle); - out.append(buffer); - out.append("\n"); - - for (const auto &it : this->characteristics) { - out.append(" characteristics: "); - it.dump_to(out); - out.append("\n"); - } - out.append("}"); -} -#endif bool BluetoothGATTGetServicesResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -7105,23 +4313,6 @@ void BluetoothGATTGetServicesResponse::calculate_size(uint32_t &total_size) cons ProtoSize::add_uint64_field(total_size, 1, this->address, false); ProtoSize::add_repeated_message(total_size, 1, this->services); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void BluetoothGATTGetServicesResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothGATTGetServicesResponse {\n"); - out.append(" address: "); - sprintf(buffer, "%llu", this->address); - out.append(buffer); - out.append("\n"); - - for (const auto &it : this->services) { - out.append(" services: "); - it.dump_to(out); - out.append("\n"); - } - out.append("}"); -} -#endif bool BluetoothGATTGetServicesDoneResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -7138,17 +4329,6 @@ void BluetoothGATTGetServicesDoneResponse::encode(ProtoWriteBuffer buffer) const void BluetoothGATTGetServicesDoneResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_uint64_field(total_size, 1, this->address, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void BluetoothGATTGetServicesDoneResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothGATTGetServicesDoneResponse {\n"); - out.append(" address: "); - sprintf(buffer, "%llu", this->address); - out.append(buffer); - out.append("\n"); - out.append("}"); -} -#endif bool BluetoothGATTReadRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -7171,22 +4351,6 @@ void BluetoothGATTReadRequest::calculate_size(uint32_t &total_size) const { ProtoSize::add_uint64_field(total_size, 1, this->address, false); ProtoSize::add_uint32_field(total_size, 1, this->handle, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void BluetoothGATTReadRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothGATTReadRequest {\n"); - out.append(" address: "); - sprintf(buffer, "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" handle: "); - sprintf(buffer, "%" PRIu32, this->handle); - out.append(buffer); - out.append("\n"); - out.append("}"); -} -#endif bool BluetoothGATTReadResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -7214,33 +4378,13 @@ bool BluetoothGATTReadResponse::decode_length(uint32_t field_id, ProtoLengthDeli void BluetoothGATTReadResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_uint32(2, this->handle); - buffer.encode_string(3, this->data); + buffer.encode_bytes(3, reinterpret_cast(this->data.data()), this->data.size()); } void BluetoothGATTReadResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_uint64_field(total_size, 1, this->address, false); ProtoSize::add_uint32_field(total_size, 1, this->handle, false); ProtoSize::add_string_field(total_size, 1, this->data, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void BluetoothGATTReadResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothGATTReadResponse {\n"); - out.append(" address: "); - sprintf(buffer, "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" handle: "); - sprintf(buffer, "%" PRIu32, this->handle); - out.append(buffer); - out.append("\n"); - - out.append(" data: "); - out.append("'").append(this->data).append("'"); - out.append("\n"); - out.append("}"); -} -#endif bool BluetoothGATTWriteRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -7273,7 +4417,7 @@ void BluetoothGATTWriteRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_uint32(2, this->handle); buffer.encode_bool(3, this->response); - buffer.encode_string(4, this->data); + buffer.encode_bytes(4, reinterpret_cast(this->data.data()), this->data.size()); } void BluetoothGATTWriteRequest::calculate_size(uint32_t &total_size) const { ProtoSize::add_uint64_field(total_size, 1, this->address, false); @@ -7281,30 +4425,6 @@ void BluetoothGATTWriteRequest::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 1, this->response, false); ProtoSize::add_string_field(total_size, 1, this->data, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void BluetoothGATTWriteRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothGATTWriteRequest {\n"); - out.append(" address: "); - sprintf(buffer, "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" handle: "); - sprintf(buffer, "%" PRIu32, this->handle); - out.append(buffer); - out.append("\n"); - - out.append(" response: "); - out.append(YESNO(this->response)); - out.append("\n"); - - out.append(" data: "); - out.append("'").append(this->data).append("'"); - out.append("\n"); - out.append("}"); -} -#endif bool BluetoothGATTReadDescriptorRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -7327,22 +4447,6 @@ void BluetoothGATTReadDescriptorRequest::calculate_size(uint32_t &total_size) co ProtoSize::add_uint64_field(total_size, 1, this->address, false); ProtoSize::add_uint32_field(total_size, 1, this->handle, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void BluetoothGATTReadDescriptorRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothGATTReadDescriptorRequest {\n"); - out.append(" address: "); - sprintf(buffer, "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" handle: "); - sprintf(buffer, "%" PRIu32, this->handle); - out.append(buffer); - out.append("\n"); - out.append("}"); -} -#endif bool BluetoothGATTWriteDescriptorRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -7370,33 +4474,13 @@ bool BluetoothGATTWriteDescriptorRequest::decode_length(uint32_t field_id, Proto void BluetoothGATTWriteDescriptorRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_uint32(2, this->handle); - buffer.encode_string(3, this->data); + buffer.encode_bytes(3, reinterpret_cast(this->data.data()), this->data.size()); } void BluetoothGATTWriteDescriptorRequest::calculate_size(uint32_t &total_size) const { ProtoSize::add_uint64_field(total_size, 1, this->address, false); ProtoSize::add_uint32_field(total_size, 1, this->handle, false); ProtoSize::add_string_field(total_size, 1, this->data, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void BluetoothGATTWriteDescriptorRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothGATTWriteDescriptorRequest {\n"); - out.append(" address: "); - sprintf(buffer, "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" handle: "); - sprintf(buffer, "%" PRIu32, this->handle); - out.append(buffer); - out.append("\n"); - - out.append(" data: "); - out.append("'").append(this->data).append("'"); - out.append("\n"); - out.append("}"); -} -#endif bool BluetoothGATTNotifyRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -7425,26 +4509,6 @@ void BluetoothGATTNotifyRequest::calculate_size(uint32_t &total_size) const { ProtoSize::add_uint32_field(total_size, 1, this->handle, false); ProtoSize::add_bool_field(total_size, 1, this->enable, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void BluetoothGATTNotifyRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothGATTNotifyRequest {\n"); - out.append(" address: "); - sprintf(buffer, "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" handle: "); - sprintf(buffer, "%" PRIu32, this->handle); - out.append(buffer); - out.append("\n"); - - out.append(" enable: "); - out.append(YESNO(this->enable)); - out.append("\n"); - out.append("}"); -} -#endif bool BluetoothGATTNotifyDataResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -7472,40 +4536,13 @@ bool BluetoothGATTNotifyDataResponse::decode_length(uint32_t field_id, ProtoLeng void BluetoothGATTNotifyDataResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_uint32(2, this->handle); - buffer.encode_string(3, this->data); + buffer.encode_bytes(3, reinterpret_cast(this->data.data()), this->data.size()); } void BluetoothGATTNotifyDataResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_uint64_field(total_size, 1, this->address, false); ProtoSize::add_uint32_field(total_size, 1, this->handle, false); ProtoSize::add_string_field(total_size, 1, this->data, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void BluetoothGATTNotifyDataResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothGATTNotifyDataResponse {\n"); - out.append(" address: "); - sprintf(buffer, "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" handle: "); - sprintf(buffer, "%" PRIu32, this->handle); - out.append(buffer); - out.append("\n"); - - out.append(" data: "); - out.append("'").append(this->data).append("'"); - out.append("\n"); - out.append("}"); -} -#endif -void SubscribeBluetoothConnectionsFreeRequest::encode(ProtoWriteBuffer buffer) const {} -void SubscribeBluetoothConnectionsFreeRequest::calculate_size(uint32_t &total_size) const {} -#ifdef HAS_PROTO_MESSAGE_DUMP -void SubscribeBluetoothConnectionsFreeRequest::dump_to(std::string &out) const { - out.append("SubscribeBluetoothConnectionsFreeRequest {}"); -} -#endif bool BluetoothConnectionsFreeResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -7540,29 +4577,6 @@ void BluetoothConnectionsFreeResponse::calculate_size(uint32_t &total_size) cons } } } -#ifdef HAS_PROTO_MESSAGE_DUMP -void BluetoothConnectionsFreeResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothConnectionsFreeResponse {\n"); - out.append(" free: "); - sprintf(buffer, "%" PRIu32, this->free); - out.append(buffer); - out.append("\n"); - - out.append(" limit: "); - sprintf(buffer, "%" PRIu32, this->limit); - out.append(buffer); - out.append("\n"); - - for (const auto &it : this->allocated) { - out.append(" allocated: "); - sprintf(buffer, "%llu", it); - out.append(buffer); - out.append("\n"); - } - out.append("}"); -} -#endif bool BluetoothGATTErrorResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -7591,27 +4605,6 @@ void BluetoothGATTErrorResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_uint32_field(total_size, 1, this->handle, false); ProtoSize::add_int32_field(total_size, 1, this->error, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void BluetoothGATTErrorResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothGATTErrorResponse {\n"); - out.append(" address: "); - sprintf(buffer, "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" handle: "); - sprintf(buffer, "%" PRIu32, this->handle); - out.append(buffer); - out.append("\n"); - - out.append(" error: "); - sprintf(buffer, "%" PRId32, this->error); - out.append(buffer); - out.append("\n"); - out.append("}"); -} -#endif bool BluetoothGATTWriteResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -7634,22 +4627,6 @@ void BluetoothGATTWriteResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_uint64_field(total_size, 1, this->address, false); ProtoSize::add_uint32_field(total_size, 1, this->handle, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void BluetoothGATTWriteResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothGATTWriteResponse {\n"); - out.append(" address: "); - sprintf(buffer, "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" handle: "); - sprintf(buffer, "%" PRIu32, this->handle); - out.append(buffer); - out.append("\n"); - out.append("}"); -} -#endif bool BluetoothGATTNotifyResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -7672,22 +4649,6 @@ void BluetoothGATTNotifyResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_uint64_field(total_size, 1, this->address, false); ProtoSize::add_uint32_field(total_size, 1, this->handle, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void BluetoothGATTNotifyResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothGATTNotifyResponse {\n"); - out.append(" address: "); - sprintf(buffer, "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" handle: "); - sprintf(buffer, "%" PRIu32, this->handle); - out.append(buffer); - out.append("\n"); - out.append("}"); -} -#endif bool BluetoothDevicePairingResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -7716,26 +4677,6 @@ void BluetoothDevicePairingResponse::calculate_size(uint32_t &total_size) const ProtoSize::add_bool_field(total_size, 1, this->paired, false); ProtoSize::add_int32_field(total_size, 1, this->error, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void BluetoothDevicePairingResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothDevicePairingResponse {\n"); - out.append(" address: "); - sprintf(buffer, "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" paired: "); - out.append(YESNO(this->paired)); - out.append("\n"); - - out.append(" error: "); - sprintf(buffer, "%" PRId32, this->error); - out.append(buffer); - out.append("\n"); - out.append("}"); -} -#endif bool BluetoothDeviceUnpairingResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -7764,33 +4705,6 @@ void BluetoothDeviceUnpairingResponse::calculate_size(uint32_t &total_size) cons ProtoSize::add_bool_field(total_size, 1, this->success, false); ProtoSize::add_int32_field(total_size, 1, this->error, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void BluetoothDeviceUnpairingResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothDeviceUnpairingResponse {\n"); - out.append(" address: "); - sprintf(buffer, "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" success: "); - out.append(YESNO(this->success)); - out.append("\n"); - - out.append(" error: "); - sprintf(buffer, "%" PRId32, this->error); - out.append(buffer); - out.append("\n"); - out.append("}"); -} -#endif -void UnsubscribeBluetoothLEAdvertisementsRequest::encode(ProtoWriteBuffer buffer) const {} -void UnsubscribeBluetoothLEAdvertisementsRequest::calculate_size(uint32_t &total_size) const {} -#ifdef HAS_PROTO_MESSAGE_DUMP -void UnsubscribeBluetoothLEAdvertisementsRequest::dump_to(std::string &out) const { - out.append("UnsubscribeBluetoothLEAdvertisementsRequest {}"); -} -#endif bool BluetoothDeviceClearCacheResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -7819,26 +4733,6 @@ void BluetoothDeviceClearCacheResponse::calculate_size(uint32_t &total_size) con ProtoSize::add_bool_field(total_size, 1, this->success, false); ProtoSize::add_int32_field(total_size, 1, this->error, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void BluetoothDeviceClearCacheResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothDeviceClearCacheResponse {\n"); - out.append(" address: "); - sprintf(buffer, "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" success: "); - out.append(YESNO(this->success)); - out.append("\n"); - - out.append(" error: "); - sprintf(buffer, "%" PRId32, this->error); - out.append(buffer); - out.append("\n"); - out.append("}"); -} -#endif bool BluetoothScannerStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -7861,20 +4755,6 @@ void BluetoothScannerStateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_enum_field(total_size, 1, static_cast(this->state), false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->mode), false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void BluetoothScannerStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothScannerStateResponse {\n"); - out.append(" state: "); - out.append(proto_enum_to_string(this->state)); - out.append("\n"); - - out.append(" mode: "); - out.append(proto_enum_to_string(this->mode)); - out.append("\n"); - out.append("}"); -} -#endif bool BluetoothScannerSetModeRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -7891,16 +4771,8 @@ void BluetoothScannerSetModeRequest::encode(ProtoWriteBuffer buffer) const { void BluetoothScannerSetModeRequest::calculate_size(uint32_t &total_size) const { ProtoSize::add_enum_field(total_size, 1, static_cast(this->mode), false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void BluetoothScannerSetModeRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothScannerSetModeRequest {\n"); - out.append(" mode: "); - out.append(proto_enum_to_string(this->mode)); - out.append("\n"); - out.append("}"); -} #endif +#ifdef USE_VOICE_ASSISTANT bool SubscribeVoiceAssistantRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -7923,21 +4795,6 @@ void SubscribeVoiceAssistantRequest::calculate_size(uint32_t &total_size) const ProtoSize::add_bool_field(total_size, 1, this->subscribe, false); ProtoSize::add_uint32_field(total_size, 1, this->flags, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void SubscribeVoiceAssistantRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("SubscribeVoiceAssistantRequest {\n"); - out.append(" subscribe: "); - out.append(YESNO(this->subscribe)); - out.append("\n"); - - out.append(" flags: "); - sprintf(buffer, "%" PRIu32, this->flags); - out.append(buffer); - out.append("\n"); - out.append("}"); -} -#endif bool VoiceAssistantAudioSettings::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -7972,27 +4829,6 @@ void VoiceAssistantAudioSettings::calculate_size(uint32_t &total_size) const { ProtoSize::add_uint32_field(total_size, 1, this->auto_gain, false); ProtoSize::add_fixed_field<4>(total_size, 1, this->volume_multiplier != 0.0f, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void VoiceAssistantAudioSettings::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("VoiceAssistantAudioSettings {\n"); - out.append(" noise_suppression_level: "); - sprintf(buffer, "%" PRIu32, this->noise_suppression_level); - out.append(buffer); - out.append("\n"); - - out.append(" auto_gain: "); - sprintf(buffer, "%" PRIu32, this->auto_gain); - out.append(buffer); - out.append("\n"); - - out.append(" volume_multiplier: "); - sprintf(buffer, "%g", this->volume_multiplier); - out.append(buffer); - out.append("\n"); - out.append("}"); -} -#endif bool VoiceAssistantRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -8039,33 +4875,6 @@ void VoiceAssistantRequest::calculate_size(uint32_t &total_size) const { ProtoSize::add_message_object(total_size, 1, this->audio_settings, false); ProtoSize::add_string_field(total_size, 1, this->wake_word_phrase, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void VoiceAssistantRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("VoiceAssistantRequest {\n"); - out.append(" start: "); - out.append(YESNO(this->start)); - out.append("\n"); - - out.append(" conversation_id: "); - out.append("'").append(this->conversation_id).append("'"); - out.append("\n"); - - out.append(" flags: "); - sprintf(buffer, "%" PRIu32, this->flags); - out.append(buffer); - out.append("\n"); - - out.append(" audio_settings: "); - this->audio_settings.dump_to(out); - out.append("\n"); - - out.append(" wake_word_phrase: "); - out.append("'").append(this->wake_word_phrase).append("'"); - out.append("\n"); - out.append("}"); -} -#endif bool VoiceAssistantResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -8088,21 +4897,6 @@ void VoiceAssistantResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_uint32_field(total_size, 1, this->port, false); ProtoSize::add_bool_field(total_size, 1, this->error, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void VoiceAssistantResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("VoiceAssistantResponse {\n"); - out.append(" port: "); - sprintf(buffer, "%" PRIu32, this->port); - out.append(buffer); - out.append("\n"); - - out.append(" error: "); - out.append(YESNO(this->error)); - out.append("\n"); - out.append("}"); -} -#endif bool VoiceAssistantEventData::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 1: { @@ -8125,20 +4919,6 @@ void VoiceAssistantEventData::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->name, false); ProtoSize::add_string_field(total_size, 1, this->value, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void VoiceAssistantEventData::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("VoiceAssistantEventData {\n"); - out.append(" name: "); - out.append("'").append(this->name).append("'"); - out.append("\n"); - - out.append(" value: "); - out.append("'").append(this->value).append("'"); - out.append("\n"); - out.append("}"); -} -#endif bool VoiceAssistantEventResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -8169,22 +4949,6 @@ void VoiceAssistantEventResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_enum_field(total_size, 1, static_cast(this->event_type), false); ProtoSize::add_repeated_message(total_size, 1, this->data); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void VoiceAssistantEventResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("VoiceAssistantEventResponse {\n"); - out.append(" event_type: "); - out.append(proto_enum_to_string(this->event_type)); - out.append("\n"); - - for (const auto &it : this->data) { - out.append(" data: "); - it.dump_to(out); - out.append("\n"); - } - out.append("}"); -} -#endif bool VoiceAssistantAudio::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -8206,27 +4970,13 @@ bool VoiceAssistantAudio::decode_length(uint32_t field_id, ProtoLengthDelimited } } void VoiceAssistantAudio::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->data); + buffer.encode_bytes(1, reinterpret_cast(this->data.data()), this->data.size()); buffer.encode_bool(2, this->end); } void VoiceAssistantAudio::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->data, false); ProtoSize::add_bool_field(total_size, 1, this->end, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void VoiceAssistantAudio::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("VoiceAssistantAudio {\n"); - out.append(" data: "); - out.append("'").append(this->data).append("'"); - out.append("\n"); - - out.append(" end: "); - out.append(YESNO(this->end)); - out.append("\n"); - out.append("}"); -} -#endif bool VoiceAssistantTimerEventResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -8279,38 +5029,6 @@ void VoiceAssistantTimerEventResponse::calculate_size(uint32_t &total_size) cons ProtoSize::add_uint32_field(total_size, 1, this->seconds_left, false); ProtoSize::add_bool_field(total_size, 1, this->is_active, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void VoiceAssistantTimerEventResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("VoiceAssistantTimerEventResponse {\n"); - out.append(" event_type: "); - out.append(proto_enum_to_string(this->event_type)); - out.append("\n"); - - out.append(" timer_id: "); - out.append("'").append(this->timer_id).append("'"); - out.append("\n"); - - out.append(" name: "); - out.append("'").append(this->name).append("'"); - out.append("\n"); - - out.append(" total_seconds: "); - sprintf(buffer, "%" PRIu32, this->total_seconds); - out.append(buffer); - out.append("\n"); - - out.append(" seconds_left: "); - sprintf(buffer, "%" PRIu32, this->seconds_left); - out.append(buffer); - out.append("\n"); - - out.append(" is_active: "); - out.append(YESNO(this->is_active)); - out.append("\n"); - out.append("}"); -} -#endif bool VoiceAssistantAnnounceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 4: { @@ -8351,28 +5069,6 @@ void VoiceAssistantAnnounceRequest::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->preannounce_media_id, false); ProtoSize::add_bool_field(total_size, 1, this->start_conversation, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void VoiceAssistantAnnounceRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("VoiceAssistantAnnounceRequest {\n"); - out.append(" media_id: "); - out.append("'").append(this->media_id).append("'"); - out.append("\n"); - - out.append(" text: "); - out.append("'").append(this->text).append("'"); - out.append("\n"); - - out.append(" preannounce_media_id: "); - out.append("'").append(this->preannounce_media_id).append("'"); - out.append("\n"); - - out.append(" start_conversation: "); - out.append(YESNO(this->start_conversation)); - out.append("\n"); - out.append("}"); -} -#endif bool VoiceAssistantAnnounceFinished::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -8387,16 +5083,6 @@ void VoiceAssistantAnnounceFinished::encode(ProtoWriteBuffer buffer) const { buf void VoiceAssistantAnnounceFinished::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 1, this->success, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void VoiceAssistantAnnounceFinished::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("VoiceAssistantAnnounceFinished {\n"); - out.append(" success: "); - out.append(YESNO(this->success)); - out.append("\n"); - out.append("}"); -} -#endif bool VoiceAssistantWakeWord::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 1: { @@ -8431,33 +5117,6 @@ void VoiceAssistantWakeWord::calculate_size(uint32_t &total_size) const { } } } -#ifdef HAS_PROTO_MESSAGE_DUMP -void VoiceAssistantWakeWord::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("VoiceAssistantWakeWord {\n"); - out.append(" id: "); - out.append("'").append(this->id).append("'"); - out.append("\n"); - - out.append(" wake_word: "); - out.append("'").append(this->wake_word).append("'"); - out.append("\n"); - - for (const auto &it : this->trained_languages) { - out.append(" trained_languages: "); - out.append("'").append(it).append("'"); - out.append("\n"); - } - out.append("}"); -} -#endif -void VoiceAssistantConfigurationRequest::encode(ProtoWriteBuffer buffer) const {} -void VoiceAssistantConfigurationRequest::calculate_size(uint32_t &total_size) const {} -#ifdef HAS_PROTO_MESSAGE_DUMP -void VoiceAssistantConfigurationRequest::dump_to(std::string &out) const { - out.append("VoiceAssistantConfigurationRequest {}"); -} -#endif bool VoiceAssistantConfigurationResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 3: { @@ -8500,29 +5159,6 @@ void VoiceAssistantConfigurationResponse::calculate_size(uint32_t &total_size) c } ProtoSize::add_uint32_field(total_size, 1, this->max_active_wake_words, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void VoiceAssistantConfigurationResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("VoiceAssistantConfigurationResponse {\n"); - for (const auto &it : this->available_wake_words) { - out.append(" available_wake_words: "); - it.dump_to(out); - out.append("\n"); - } - - for (const auto &it : this->active_wake_words) { - out.append(" active_wake_words: "); - out.append("'").append(it).append("'"); - out.append("\n"); - } - - out.append(" max_active_wake_words: "); - sprintf(buffer, "%" PRIu32, this->max_active_wake_words); - out.append(buffer); - out.append("\n"); - out.append("}"); -} -#endif bool VoiceAssistantSetConfiguration::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 1: { @@ -8545,18 +5181,8 @@ void VoiceAssistantSetConfiguration::calculate_size(uint32_t &total_size) const } } } -#ifdef HAS_PROTO_MESSAGE_DUMP -void VoiceAssistantSetConfiguration::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("VoiceAssistantSetConfiguration {\n"); - for (const auto &it : this->active_wake_words) { - out.append(" active_wake_words: "); - out.append("'").append(it).append("'"); - out.append("\n"); - } - out.append("}"); -} #endif +#ifdef USE_ALARM_CONTROL_PANEL bool ListEntitiesAlarmControlPanelResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 6: { @@ -8579,6 +5205,10 @@ bool ListEntitiesAlarmControlPanelResponse::decode_varint(uint32_t field_id, Pro this->requires_code_to_arm = value.as_bool(); return true; } + case 11: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -8626,6 +5256,7 @@ void ListEntitiesAlarmControlPanelResponse::encode(ProtoWriteBuffer buffer) cons buffer.encode_uint32(8, this->supported_features); buffer.encode_bool(9, this->requires_code); buffer.encode_bool(10, this->requires_code_to_arm); + buffer.encode_uint32(11, this->device_id); } void ListEntitiesAlarmControlPanelResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -8638,61 +5269,18 @@ void ListEntitiesAlarmControlPanelResponse::calculate_size(uint32_t &total_size) ProtoSize::add_uint32_field(total_size, 1, this->supported_features, false); ProtoSize::add_bool_field(total_size, 1, this->requires_code, false); ProtoSize::add_bool_field(total_size, 1, this->requires_code_to_arm, false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void ListEntitiesAlarmControlPanelResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesAlarmControlPanelResponse {\n"); - out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); - out.append("\n"); - - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - out.append("'").append(this->name).append("'"); - out.append("\n"); - - out.append(" unique_id: "); - out.append("'").append(this->unique_id).append("'"); - out.append("\n"); - - out.append(" icon: "); - out.append("'").append(this->icon).append("'"); - out.append("\n"); - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - - out.append(" supported_features: "); - sprintf(buffer, "%" PRIu32, this->supported_features); - out.append(buffer); - out.append("\n"); - - out.append(" requires_code: "); - out.append(YESNO(this->requires_code)); - out.append("\n"); - - out.append(" requires_code_to_arm: "); - out.append(YESNO(this->requires_code_to_arm)); - out.append("\n"); - out.append("}"); -} -#endif bool AlarmControlPanelStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { this->state = value.as_enum(); return true; } + case 3: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -8710,26 +5298,13 @@ bool AlarmControlPanelStateResponse::decode_32bit(uint32_t field_id, Proto32Bit void AlarmControlPanelStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_enum(2, this->state); + buffer.encode_uint32(3, this->device_id); } void AlarmControlPanelStateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->state), false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void AlarmControlPanelStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("AlarmControlPanelStateResponse {\n"); - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - out.append(proto_enum_to_string(this->state)); - out.append("\n"); - out.append("}"); -} -#endif bool AlarmControlPanelCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -8770,25 +5345,8 @@ void AlarmControlPanelCommandRequest::calculate_size(uint32_t &total_size) const ProtoSize::add_enum_field(total_size, 1, static_cast(this->command), false); ProtoSize::add_string_field(total_size, 1, this->code, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void AlarmControlPanelCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("AlarmControlPanelCommandRequest {\n"); - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" command: "); - out.append(proto_enum_to_string(this->command)); - out.append("\n"); - - out.append(" code: "); - out.append("'").append(this->code).append("'"); - out.append("\n"); - out.append("}"); -} #endif +#ifdef USE_TEXT bool ListEntitiesTextResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 6: { @@ -8811,6 +5369,10 @@ bool ListEntitiesTextResponse::decode_varint(uint32_t field_id, ProtoVarInt valu this->mode = value.as_enum(); return true; } + case 12: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -8863,6 +5425,7 @@ void ListEntitiesTextResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(9, this->max_length); buffer.encode_string(10, this->pattern); buffer.encode_enum(11, this->mode); + buffer.encode_uint32(12, this->device_id); } void ListEntitiesTextResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -8876,66 +5439,18 @@ void ListEntitiesTextResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_uint32_field(total_size, 1, this->max_length, false); ProtoSize::add_string_field(total_size, 1, this->pattern, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->mode), false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void ListEntitiesTextResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesTextResponse {\n"); - out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); - out.append("\n"); - - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - out.append("'").append(this->name).append("'"); - out.append("\n"); - - out.append(" unique_id: "); - out.append("'").append(this->unique_id).append("'"); - out.append("\n"); - - out.append(" icon: "); - out.append("'").append(this->icon).append("'"); - out.append("\n"); - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - - out.append(" min_length: "); - sprintf(buffer, "%" PRIu32, this->min_length); - out.append(buffer); - out.append("\n"); - - out.append(" max_length: "); - sprintf(buffer, "%" PRIu32, this->max_length); - out.append(buffer); - out.append("\n"); - - out.append(" pattern: "); - out.append("'").append(this->pattern).append("'"); - out.append("\n"); - - out.append(" mode: "); - out.append(proto_enum_to_string(this->mode)); - out.append("\n"); - out.append("}"); -} -#endif bool TextStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 3: { this->missing_state = value.as_bool(); return true; } + case 4: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -8964,31 +5479,14 @@ void TextStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_string(2, this->state); buffer.encode_bool(3, this->missing_state); + buffer.encode_uint32(4, this->device_id); } void TextStateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); ProtoSize::add_string_field(total_size, 1, this->state, false); ProtoSize::add_bool_field(total_size, 1, this->missing_state, false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void TextStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("TextStateResponse {\n"); - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - out.append("'").append(this->state).append("'"); - out.append("\n"); - - out.append(" missing_state: "); - out.append(YESNO(this->missing_state)); - out.append("\n"); - out.append("}"); -} -#endif bool TextCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 2: { @@ -9017,21 +5515,8 @@ void TextCommandRequest::calculate_size(uint32_t &total_size) const { ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); ProtoSize::add_string_field(total_size, 1, this->state, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void TextCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("TextCommandRequest {\n"); - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - out.append("'").append(this->state).append("'"); - out.append("\n"); - out.append("}"); -} #endif +#ifdef USE_DATETIME_DATE bool ListEntitiesDateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 6: { @@ -9042,6 +5527,10 @@ bool ListEntitiesDateResponse::decode_varint(uint32_t field_id, ProtoVarInt valu this->entity_category = value.as_enum(); return true; } + case 8: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -9086,6 +5575,7 @@ void ListEntitiesDateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(5, this->icon); buffer.encode_bool(6, this->disabled_by_default); buffer.encode_enum(7, this->entity_category); + buffer.encode_uint32(8, this->device_id); } void ListEntitiesDateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -9095,42 +5585,8 @@ void ListEntitiesDateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->icon, false); ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void ListEntitiesDateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesDateResponse {\n"); - out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); - out.append("\n"); - - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - out.append("'").append(this->name).append("'"); - out.append("\n"); - - out.append(" unique_id: "); - out.append("'").append(this->unique_id).append("'"); - out.append("\n"); - - out.append(" icon: "); - out.append("'").append(this->icon).append("'"); - out.append("\n"); - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - out.append("}"); -} -#endif bool DateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -9149,6 +5605,10 @@ bool DateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { this->day = value.as_uint32(); return true; } + case 6: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -9169,6 +5629,7 @@ void DateStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(3, this->year); buffer.encode_uint32(4, this->month); buffer.encode_uint32(5, this->day); + buffer.encode_uint32(6, this->device_id); } void DateStateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); @@ -9176,37 +5637,8 @@ void DateStateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_uint32_field(total_size, 1, this->year, false); ProtoSize::add_uint32_field(total_size, 1, this->month, false); ProtoSize::add_uint32_field(total_size, 1, this->day, false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void DateStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("DateStateResponse {\n"); - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" missing_state: "); - out.append(YESNO(this->missing_state)); - out.append("\n"); - - out.append(" year: "); - sprintf(buffer, "%" PRIu32, this->year); - out.append(buffer); - out.append("\n"); - - out.append(" month: "); - sprintf(buffer, "%" PRIu32, this->month); - out.append(buffer); - out.append("\n"); - - out.append(" day: "); - sprintf(buffer, "%" PRIu32, this->day); - out.append(buffer); - out.append("\n"); - out.append("}"); -} -#endif bool DateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -9247,32 +5679,8 @@ void DateCommandRequest::calculate_size(uint32_t &total_size) const { ProtoSize::add_uint32_field(total_size, 1, this->month, false); ProtoSize::add_uint32_field(total_size, 1, this->day, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void DateCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("DateCommandRequest {\n"); - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" year: "); - sprintf(buffer, "%" PRIu32, this->year); - out.append(buffer); - out.append("\n"); - - out.append(" month: "); - sprintf(buffer, "%" PRIu32, this->month); - out.append(buffer); - out.append("\n"); - - out.append(" day: "); - sprintf(buffer, "%" PRIu32, this->day); - out.append(buffer); - out.append("\n"); - out.append("}"); -} #endif +#ifdef USE_DATETIME_TIME bool ListEntitiesTimeResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 6: { @@ -9283,6 +5691,10 @@ bool ListEntitiesTimeResponse::decode_varint(uint32_t field_id, ProtoVarInt valu this->entity_category = value.as_enum(); return true; } + case 8: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -9327,6 +5739,7 @@ void ListEntitiesTimeResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(5, this->icon); buffer.encode_bool(6, this->disabled_by_default); buffer.encode_enum(7, this->entity_category); + buffer.encode_uint32(8, this->device_id); } void ListEntitiesTimeResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -9336,42 +5749,8 @@ void ListEntitiesTimeResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->icon, false); ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void ListEntitiesTimeResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesTimeResponse {\n"); - out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); - out.append("\n"); - - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - out.append("'").append(this->name).append("'"); - out.append("\n"); - - out.append(" unique_id: "); - out.append("'").append(this->unique_id).append("'"); - out.append("\n"); - - out.append(" icon: "); - out.append("'").append(this->icon).append("'"); - out.append("\n"); - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - out.append("}"); -} -#endif bool TimeStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -9390,6 +5769,10 @@ bool TimeStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { this->second = value.as_uint32(); return true; } + case 6: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -9410,6 +5793,7 @@ void TimeStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(3, this->hour); buffer.encode_uint32(4, this->minute); buffer.encode_uint32(5, this->second); + buffer.encode_uint32(6, this->device_id); } void TimeStateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); @@ -9417,37 +5801,8 @@ void TimeStateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_uint32_field(total_size, 1, this->hour, false); ProtoSize::add_uint32_field(total_size, 1, this->minute, false); ProtoSize::add_uint32_field(total_size, 1, this->second, false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void TimeStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("TimeStateResponse {\n"); - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" missing_state: "); - out.append(YESNO(this->missing_state)); - out.append("\n"); - - out.append(" hour: "); - sprintf(buffer, "%" PRIu32, this->hour); - out.append(buffer); - out.append("\n"); - - out.append(" minute: "); - sprintf(buffer, "%" PRIu32, this->minute); - out.append(buffer); - out.append("\n"); - - out.append(" second: "); - sprintf(buffer, "%" PRIu32, this->second); - out.append(buffer); - out.append("\n"); - out.append("}"); -} -#endif bool TimeCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -9488,32 +5843,8 @@ void TimeCommandRequest::calculate_size(uint32_t &total_size) const { ProtoSize::add_uint32_field(total_size, 1, this->minute, false); ProtoSize::add_uint32_field(total_size, 1, this->second, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void TimeCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("TimeCommandRequest {\n"); - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" hour: "); - sprintf(buffer, "%" PRIu32, this->hour); - out.append(buffer); - out.append("\n"); - - out.append(" minute: "); - sprintf(buffer, "%" PRIu32, this->minute); - out.append(buffer); - out.append("\n"); - - out.append(" second: "); - sprintf(buffer, "%" PRIu32, this->second); - out.append(buffer); - out.append("\n"); - out.append("}"); -} #endif +#ifdef USE_EVENT bool ListEntitiesEventResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 6: { @@ -9524,6 +5855,10 @@ bool ListEntitiesEventResponse::decode_varint(uint32_t field_id, ProtoVarInt val this->entity_category = value.as_enum(); return true; } + case 10: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -9580,6 +5915,7 @@ void ListEntitiesEventResponse::encode(ProtoWriteBuffer buffer) const { for (auto &it : this->event_types) { buffer.encode_string(9, it, true); } + buffer.encode_uint32(10, this->device_id); } void ListEntitiesEventResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -9595,52 +5931,18 @@ void ListEntitiesEventResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, it, true); } } + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void ListEntitiesEventResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesEventResponse {\n"); - out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); - out.append("\n"); - - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - out.append("'").append(this->name).append("'"); - out.append("\n"); - - out.append(" unique_id: "); - out.append("'").append(this->unique_id).append("'"); - out.append("\n"); - - out.append(" icon: "); - out.append("'").append(this->icon).append("'"); - out.append("\n"); - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - - out.append(" device_class: "); - out.append("'").append(this->device_class).append("'"); - out.append("\n"); - - for (const auto &it : this->event_types) { - out.append(" event_types: "); - out.append("'").append(it).append("'"); - out.append("\n"); +bool EventResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 3: { + this->device_id = value.as_uint32(); + return true; + } + default: + return false; } - out.append("}"); } -#endif bool EventResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 2: { @@ -9664,26 +5966,15 @@ bool EventResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { void EventResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_string(2, this->event_type); + buffer.encode_uint32(3, this->device_id); } void EventResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); ProtoSize::add_string_field(total_size, 1, this->event_type, false); -} -#ifdef HAS_PROTO_MESSAGE_DUMP -void EventResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("EventResponse {\n"); - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" event_type: "); - out.append("'").append(this->event_type).append("'"); - out.append("\n"); - out.append("}"); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } #endif +#ifdef USE_VALVE bool ListEntitiesValveResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 6: { @@ -9706,6 +5997,10 @@ bool ListEntitiesValveResponse::decode_varint(uint32_t field_id, ProtoVarInt val this->supports_stop = value.as_bool(); return true; } + case 12: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -9758,6 +6053,7 @@ void ListEntitiesValveResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(9, this->assumed_state); buffer.encode_bool(10, this->supports_position); buffer.encode_bool(11, this->supports_stop); + buffer.encode_uint32(12, this->device_id); } void ListEntitiesValveResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -9771,64 +6067,18 @@ void ListEntitiesValveResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 1, this->assumed_state, false); ProtoSize::add_bool_field(total_size, 1, this->supports_position, false); ProtoSize::add_bool_field(total_size, 1, this->supports_stop, false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void ListEntitiesValveResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesValveResponse {\n"); - out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); - out.append("\n"); - - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - out.append("'").append(this->name).append("'"); - out.append("\n"); - - out.append(" unique_id: "); - out.append("'").append(this->unique_id).append("'"); - out.append("\n"); - - out.append(" icon: "); - out.append("'").append(this->icon).append("'"); - out.append("\n"); - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - - out.append(" device_class: "); - out.append("'").append(this->device_class).append("'"); - out.append("\n"); - - out.append(" assumed_state: "); - out.append(YESNO(this->assumed_state)); - out.append("\n"); - - out.append(" supports_position: "); - out.append(YESNO(this->supports_position)); - out.append("\n"); - - out.append(" supports_stop: "); - out.append(YESNO(this->supports_stop)); - out.append("\n"); - out.append("}"); -} -#endif bool ValveStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 3: { this->current_operation = value.as_enum(); return true; } + case 4: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -9851,32 +6101,14 @@ void ValveStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_float(2, this->position); buffer.encode_enum(3, this->current_operation); + buffer.encode_uint32(4, this->device_id); } void ValveStateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); ProtoSize::add_fixed_field<4>(total_size, 1, this->position != 0.0f, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->current_operation), false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void ValveStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ValveStateResponse {\n"); - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" position: "); - sprintf(buffer, "%g", this->position); - out.append(buffer); - out.append("\n"); - - out.append(" current_operation: "); - out.append(proto_enum_to_string(this->current_operation)); - out.append("\n"); - out.append("}"); -} -#endif bool ValveCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -9917,30 +6149,8 @@ void ValveCommandRequest::calculate_size(uint32_t &total_size) const { ProtoSize::add_fixed_field<4>(total_size, 1, this->position != 0.0f, false); ProtoSize::add_bool_field(total_size, 1, this->stop, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void ValveCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ValveCommandRequest {\n"); - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" has_position: "); - out.append(YESNO(this->has_position)); - out.append("\n"); - - out.append(" position: "); - sprintf(buffer, "%g", this->position); - out.append(buffer); - out.append("\n"); - - out.append(" stop: "); - out.append(YESNO(this->stop)); - out.append("\n"); - out.append("}"); -} #endif +#ifdef USE_DATETIME_DATETIME bool ListEntitiesDateTimeResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 6: { @@ -9951,6 +6161,10 @@ bool ListEntitiesDateTimeResponse::decode_varint(uint32_t field_id, ProtoVarInt this->entity_category = value.as_enum(); return true; } + case 8: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -9995,6 +6209,7 @@ void ListEntitiesDateTimeResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(5, this->icon); buffer.encode_bool(6, this->disabled_by_default); buffer.encode_enum(7, this->entity_category); + buffer.encode_uint32(8, this->device_id); } void ListEntitiesDateTimeResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -10004,48 +6219,18 @@ void ListEntitiesDateTimeResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->icon, false); ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void ListEntitiesDateTimeResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesDateTimeResponse {\n"); - out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); - out.append("\n"); - - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - out.append("'").append(this->name).append("'"); - out.append("\n"); - - out.append(" unique_id: "); - out.append("'").append(this->unique_id).append("'"); - out.append("\n"); - - out.append(" icon: "); - out.append("'").append(this->icon).append("'"); - out.append("\n"); - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - out.append("}"); -} -#endif bool DateTimeStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { this->missing_state = value.as_bool(); return true; } + case 4: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -10068,32 +6253,14 @@ void DateTimeStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->missing_state); buffer.encode_fixed32(3, this->epoch_seconds); + buffer.encode_uint32(4, this->device_id); } void DateTimeStateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); ProtoSize::add_bool_field(total_size, 1, this->missing_state, false); ProtoSize::add_fixed_field<4>(total_size, 1, this->epoch_seconds != 0, false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void DateTimeStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("DateTimeStateResponse {\n"); - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" missing_state: "); - out.append(YESNO(this->missing_state)); - out.append("\n"); - - out.append(" epoch_seconds: "); - sprintf(buffer, "%" PRIu32, this->epoch_seconds); - out.append(buffer); - out.append("\n"); - out.append("}"); -} -#endif bool DateTimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { case 1: { @@ -10116,22 +6283,8 @@ void DateTimeCommandRequest::calculate_size(uint32_t &total_size) const { ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); ProtoSize::add_fixed_field<4>(total_size, 1, this->epoch_seconds != 0, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void DateTimeCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("DateTimeCommandRequest {\n"); - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" epoch_seconds: "); - sprintf(buffer, "%" PRIu32, this->epoch_seconds); - out.append(buffer); - out.append("\n"); - out.append("}"); -} #endif +#ifdef USE_UPDATE bool ListEntitiesUpdateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 6: { @@ -10142,6 +6295,10 @@ bool ListEntitiesUpdateResponse::decode_varint(uint32_t field_id, ProtoVarInt va this->entity_category = value.as_enum(); return true; } + case 9: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -10191,6 +6348,7 @@ void ListEntitiesUpdateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(6, this->disabled_by_default); buffer.encode_enum(7, this->entity_category); buffer.encode_string(8, this->device_class); + buffer.encode_uint32(9, this->device_id); } void ListEntitiesUpdateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id, false); @@ -10201,46 +6359,8 @@ void ListEntitiesUpdateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); ProtoSize::add_string_field(total_size, 1, this->device_class, false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void ListEntitiesUpdateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesUpdateResponse {\n"); - out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); - out.append("\n"); - - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - out.append("'").append(this->name).append("'"); - out.append("\n"); - - out.append(" unique_id: "); - out.append("'").append(this->unique_id).append("'"); - out.append("\n"); - - out.append(" icon: "); - out.append("'").append(this->icon).append("'"); - out.append("\n"); - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - - out.append(" device_class: "); - out.append("'").append(this->device_class).append("'"); - out.append("\n"); - out.append("}"); -} -#endif bool UpdateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -10255,6 +6375,10 @@ bool UpdateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { this->has_progress = value.as_bool(); return true; } + case 11: { + this->device_id = value.as_uint32(); + return true; + } default: return false; } @@ -10310,6 +6434,7 @@ void UpdateStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(8, this->title); buffer.encode_string(9, this->release_summary); buffer.encode_string(10, this->release_url); + buffer.encode_uint32(11, this->device_id); } void UpdateStateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); @@ -10322,55 +6447,8 @@ void UpdateStateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->title, false); ProtoSize::add_string_field(total_size, 1, this->release_summary, false); ProtoSize::add_string_field(total_size, 1, this->release_url, false); + ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void UpdateStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("UpdateStateResponse {\n"); - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" missing_state: "); - out.append(YESNO(this->missing_state)); - out.append("\n"); - - out.append(" in_progress: "); - out.append(YESNO(this->in_progress)); - out.append("\n"); - - out.append(" has_progress: "); - out.append(YESNO(this->has_progress)); - out.append("\n"); - - out.append(" progress: "); - sprintf(buffer, "%g", this->progress); - out.append(buffer); - out.append("\n"); - - out.append(" current_version: "); - out.append("'").append(this->current_version).append("'"); - out.append("\n"); - - out.append(" latest_version: "); - out.append("'").append(this->latest_version).append("'"); - out.append("\n"); - - out.append(" title: "); - out.append("'").append(this->title).append("'"); - out.append("\n"); - - out.append(" release_summary: "); - out.append("'").append(this->release_summary).append("'"); - out.append("\n"); - - out.append(" release_url: "); - out.append("'").append(this->release_url).append("'"); - out.append("\n"); - out.append("}"); -} -#endif bool UpdateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -10399,20 +6477,6 @@ void UpdateCommandRequest::calculate_size(uint32_t &total_size) const { ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); ProtoSize::add_enum_field(total_size, 1, static_cast(this->command), false); } -#ifdef HAS_PROTO_MESSAGE_DUMP -void UpdateCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("UpdateCommandRequest {\n"); - out.append(" key: "); - sprintf(buffer, "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" command: "); - out.append(proto_enum_to_string(this->command)); - out.append("\n"); - out.append("}"); -} #endif } // namespace api diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 9d270bcdc1..3bfc5f1cf4 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -2,6 +2,8 @@ // See script/api_protobuf/api_protobuf.py #pragma once +#include "esphome/core/defines.h" + #include "proto.h" #include "api_pb2_size.h" @@ -15,6 +17,7 @@ enum EntityCategory : uint32_t { ENTITY_CATEGORY_CONFIG = 1, ENTITY_CATEGORY_DIAGNOSTIC = 2, }; +#ifdef USE_COVER enum LegacyCoverState : uint32_t { LEGACY_COVER_STATE_OPEN = 0, LEGACY_COVER_STATE_CLOSED = 1, @@ -29,6 +32,8 @@ enum LegacyCoverCommand : uint32_t { LEGACY_COVER_COMMAND_CLOSE = 1, LEGACY_COVER_COMMAND_STOP = 2, }; +#endif +#ifdef USE_FAN enum FanSpeed : uint32_t { FAN_SPEED_LOW = 0, FAN_SPEED_MEDIUM = 1, @@ -38,6 +43,8 @@ enum FanDirection : uint32_t { FAN_DIRECTION_FORWARD = 0, FAN_DIRECTION_REVERSE = 1, }; +#endif +#ifdef USE_LIGHT enum ColorMode : uint32_t { COLOR_MODE_UNKNOWN = 0, COLOR_MODE_ON_OFF = 1, @@ -51,6 +58,8 @@ enum ColorMode : uint32_t { COLOR_MODE_RGB_COLOR_TEMPERATURE = 47, COLOR_MODE_RGB_COLD_WARM_WHITE = 51, }; +#endif +#ifdef USE_SENSOR enum SensorStateClass : uint32_t { STATE_CLASS_NONE = 0, STATE_CLASS_MEASUREMENT = 1, @@ -62,6 +71,7 @@ enum SensorLastResetType : uint32_t { LAST_RESET_NEVER = 1, LAST_RESET_AUTO = 2, }; +#endif enum LogLevel : uint32_t { LOG_LEVEL_NONE = 0, LOG_LEVEL_ERROR = 1, @@ -82,6 +92,7 @@ enum ServiceArgType : uint32_t { SERVICE_ARG_TYPE_FLOAT_ARRAY = 6, SERVICE_ARG_TYPE_STRING_ARRAY = 7, }; +#ifdef USE_CLIMATE enum ClimateMode : uint32_t { CLIMATE_MODE_OFF = 0, CLIMATE_MODE_HEAT_COOL = 1, @@ -127,11 +138,15 @@ enum ClimatePreset : uint32_t { CLIMATE_PRESET_SLEEP = 6, CLIMATE_PRESET_ACTIVITY = 7, }; +#endif +#ifdef USE_NUMBER enum NumberMode : uint32_t { NUMBER_MODE_AUTO = 0, NUMBER_MODE_BOX = 1, NUMBER_MODE_SLIDER = 2, }; +#endif +#ifdef USE_LOCK enum LockState : uint32_t { LOCK_STATE_NONE = 0, LOCK_STATE_LOCKED = 1, @@ -145,6 +160,8 @@ enum LockCommand : uint32_t { LOCK_LOCK = 1, LOCK_OPEN = 2, }; +#endif +#ifdef USE_MEDIA_PLAYER enum MediaPlayerState : uint32_t { MEDIA_PLAYER_STATE_NONE = 0, MEDIA_PLAYER_STATE_IDLE = 1, @@ -162,6 +179,8 @@ enum MediaPlayerFormatPurpose : uint32_t { MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT = 0, MEDIA_PLAYER_FORMAT_PURPOSE_ANNOUNCEMENT = 1, }; +#endif +#ifdef USE_BLUETOOTH_PROXY enum BluetoothDeviceRequestType : uint32_t { BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT = 0, BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT = 1, @@ -183,6 +202,7 @@ enum BluetoothScannerMode : uint32_t { BLUETOOTH_SCANNER_MODE_PASSIVE = 0, BLUETOOTH_SCANNER_MODE_ACTIVE = 1, }; +#endif enum VoiceAssistantSubscribeFlag : uint32_t { VOICE_ASSISTANT_SUBSCRIBE_NONE = 0, VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO = 1, @@ -192,6 +212,7 @@ enum VoiceAssistantRequestFlag : uint32_t { VOICE_ASSISTANT_REQUEST_USE_VAD = 1, VOICE_ASSISTANT_REQUEST_USE_WAKE_WORD = 2, }; +#ifdef USE_VOICE_ASSISTANT enum VoiceAssistantEvent : uint32_t { VOICE_ASSISTANT_ERROR = 0, VOICE_ASSISTANT_RUN_START = 1, @@ -216,6 +237,8 @@ enum VoiceAssistantTimerEvent : uint32_t { VOICE_ASSISTANT_TIMER_CANCELLED = 2, VOICE_ASSISTANT_TIMER_FINISHED = 3, }; +#endif +#ifdef USE_ALARM_CONTROL_PANEL enum AlarmControlPanelState : uint32_t { ALARM_STATE_DISARMED = 0, ALARM_STATE_ARMED_HOME = 1, @@ -237,20 +260,27 @@ enum AlarmControlPanelStateCommand : uint32_t { ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS = 5, ALARM_CONTROL_PANEL_TRIGGER = 6, }; +#endif +#ifdef USE_TEXT enum TextMode : uint32_t { TEXT_MODE_TEXT = 0, TEXT_MODE_PASSWORD = 1, }; +#endif +#ifdef USE_VALVE enum ValveOperation : uint32_t { VALVE_OPERATION_IDLE = 0, VALVE_OPERATION_IS_OPENING = 1, VALVE_OPERATION_IS_CLOSING = 2, }; +#endif +#ifdef USE_UPDATE enum UpdateCommand : uint32_t { UPDATE_COMMAND_NONE = 0, UPDATE_COMMAND_UPDATE = 1, UPDATE_COMMAND_CHECK = 2, }; +#endif } // namespace enums @@ -264,6 +294,7 @@ class InfoResponseProtoMessage : public ProtoMessage { bool disabled_by_default{false}; std::string icon{}; enums::EntityCategory entity_category{}; + uint32_t device_id{0}; protected: }; @@ -272,6 +303,7 @@ class StateResponseProtoMessage : public ProtoMessage { public: ~StateResponseProtoMessage() override = default; uint32_t key{0}; + uint32_t device_id{0}; protected: }; @@ -280,7 +312,7 @@ class HelloRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 1; static constexpr uint16_t ESTIMATED_SIZE = 17; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "hello_request"; } + const char *message_name() const override { return "hello_request"; } #endif std::string client_info{}; uint32_t api_version_major{0}; @@ -300,7 +332,7 @@ class HelloResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 2; static constexpr uint16_t ESTIMATED_SIZE = 26; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "hello_response"; } + const char *message_name() const override { return "hello_response"; } #endif uint32_t api_version_major{0}; uint32_t api_version_minor{0}; @@ -321,7 +353,7 @@ class ConnectRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 3; static constexpr uint16_t ESTIMATED_SIZE = 9; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "connect_request"; } + const char *message_name() const override { return "connect_request"; } #endif std::string password{}; void encode(ProtoWriteBuffer buffer) const override; @@ -338,7 +370,7 @@ class ConnectResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 4; static constexpr uint16_t ESTIMATED_SIZE = 2; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "connect_response"; } + const char *message_name() const override { return "connect_response"; } #endif bool invalid_password{false}; void encode(ProtoWriteBuffer buffer) const override; @@ -355,10 +387,8 @@ class DisconnectRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 5; static constexpr uint16_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "disconnect_request"; } + const char *message_name() const override { return "disconnect_request"; } #endif - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -370,10 +400,8 @@ class DisconnectResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 6; static constexpr uint16_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "disconnect_response"; } + const char *message_name() const override { return "disconnect_response"; } #endif - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -385,10 +413,8 @@ class PingRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 7; static constexpr uint16_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "ping_request"; } + const char *message_name() const override { return "ping_request"; } #endif - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -400,10 +426,8 @@ class PingResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 8; static constexpr uint16_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "ping_response"; } + const char *message_name() const override { return "ping_response"; } #endif - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -415,8 +439,18 @@ class DeviceInfoRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 9; static constexpr uint16_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "device_info_request"; } + const char *message_name() const override { return "device_info_request"; } #endif +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: +}; +class AreaInfo : public ProtoMessage { + public: + uint32_t area_id{0}; + std::string name{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -424,13 +458,30 @@ class DeviceInfoRequest : public ProtoMessage { #endif protected: + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class DeviceInfo : public ProtoMessage { + public: + uint32_t device_id{0}; + std::string name{}; + uint32_t area_id{0}; + void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(uint32_t &total_size) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class DeviceInfoResponse : public ProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 10; - static constexpr uint16_t ESTIMATED_SIZE = 129; + static constexpr uint16_t ESTIMATED_SIZE = 219; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "device_info_response"; } + const char *message_name() const override { return "device_info_response"; } #endif bool uses_password{false}; std::string name{}; @@ -451,6 +502,9 @@ class DeviceInfoResponse : public ProtoMessage { std::string suggested_area{}; std::string bluetooth_mac_address{}; bool api_encryption_supported{false}; + std::vector devices{}; + std::vector areas{}; + AreaInfo area{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -466,10 +520,8 @@ class ListEntitiesRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 11; static constexpr uint16_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_request"; } + const char *message_name() const override { return "list_entities_request"; } #endif - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -481,10 +533,8 @@ class ListEntitiesDoneResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 19; static constexpr uint16_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_done_response"; } + const char *message_name() const override { return "list_entities_done_response"; } #endif - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -496,22 +546,21 @@ class SubscribeStatesRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 20; static constexpr uint16_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "subscribe_states_request"; } + const char *message_name() const override { return "subscribe_states_request"; } #endif - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif protected: }; +#ifdef USE_BINARY_SENSOR class ListEntitiesBinarySensorResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 12; - static constexpr uint16_t ESTIMATED_SIZE = 56; + static constexpr uint16_t ESTIMATED_SIZE = 60; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_binary_sensor_response"; } + const char *message_name() const override { return "list_entities_binary_sensor_response"; } #endif std::string device_class{}; bool is_status_binary_sensor{false}; @@ -529,9 +578,9 @@ class ListEntitiesBinarySensorResponse : public InfoResponseProtoMessage { class BinarySensorStateResponse : public StateResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 21; - static constexpr uint16_t ESTIMATED_SIZE = 9; + static constexpr uint16_t ESTIMATED_SIZE = 13; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "binary_sensor_state_response"; } + const char *message_name() const override { return "binary_sensor_state_response"; } #endif bool state{false}; bool missing_state{false}; @@ -545,12 +594,14 @@ class BinarySensorStateResponse : public StateResponseProtoMessage { bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; +#endif +#ifdef USE_COVER class ListEntitiesCoverResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 13; - static constexpr uint16_t ESTIMATED_SIZE = 62; + static constexpr uint16_t ESTIMATED_SIZE = 66; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_cover_response"; } + const char *message_name() const override { return "list_entities_cover_response"; } #endif bool assumed_state{false}; bool supports_position{false}; @@ -571,9 +622,9 @@ class ListEntitiesCoverResponse : public InfoResponseProtoMessage { class CoverStateResponse : public StateResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 22; - static constexpr uint16_t ESTIMATED_SIZE = 19; + static constexpr uint16_t ESTIMATED_SIZE = 23; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "cover_state_response"; } + const char *message_name() const override { return "cover_state_response"; } #endif enums::LegacyCoverState legacy_state{}; float position{0.0f}; @@ -594,7 +645,7 @@ class CoverCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 30; static constexpr uint16_t ESTIMATED_SIZE = 25; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "cover_command_request"; } + const char *message_name() const override { return "cover_command_request"; } #endif uint32_t key{0}; bool has_legacy_command{false}; @@ -614,12 +665,14 @@ class CoverCommandRequest : public ProtoMessage { bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; +#endif +#ifdef USE_FAN class ListEntitiesFanResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 14; - static constexpr uint16_t ESTIMATED_SIZE = 73; + static constexpr uint16_t ESTIMATED_SIZE = 77; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_fan_response"; } + const char *message_name() const override { return "list_entities_fan_response"; } #endif bool supports_oscillation{false}; bool supports_speed{false}; @@ -640,9 +693,9 @@ class ListEntitiesFanResponse : public InfoResponseProtoMessage { class FanStateResponse : public StateResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 23; - static constexpr uint16_t ESTIMATED_SIZE = 26; + static constexpr uint16_t ESTIMATED_SIZE = 30; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "fan_state_response"; } + const char *message_name() const override { return "fan_state_response"; } #endif bool state{false}; bool oscillating{false}; @@ -666,7 +719,7 @@ class FanCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 31; static constexpr uint16_t ESTIMATED_SIZE = 38; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "fan_command_request"; } + const char *message_name() const override { return "fan_command_request"; } #endif uint32_t key{0}; bool has_state{false}; @@ -692,12 +745,14 @@ class FanCommandRequest : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; +#endif +#ifdef USE_LIGHT class ListEntitiesLightResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 15; - static constexpr uint16_t ESTIMATED_SIZE = 85; + static constexpr uint16_t ESTIMATED_SIZE = 90; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_light_response"; } + const char *message_name() const override { return "list_entities_light_response"; } #endif std::vector supported_color_modes{}; bool legacy_supports_brightness{false}; @@ -721,9 +776,9 @@ class ListEntitiesLightResponse : public InfoResponseProtoMessage { class LightStateResponse : public StateResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 24; - static constexpr uint16_t ESTIMATED_SIZE = 63; + static constexpr uint16_t ESTIMATED_SIZE = 67; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "light_state_response"; } + const char *message_name() const override { return "light_state_response"; } #endif bool state{false}; float brightness{0.0f}; @@ -753,7 +808,7 @@ class LightCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 32; static constexpr uint16_t ESTIMATED_SIZE = 107; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "light_command_request"; } + const char *message_name() const override { return "light_command_request"; } #endif uint32_t key{0}; bool has_state{false}; @@ -793,12 +848,14 @@ class LightCommandRequest : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; +#endif +#ifdef USE_SENSOR class ListEntitiesSensorResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 16; - static constexpr uint16_t ESTIMATED_SIZE = 73; + static constexpr uint16_t ESTIMATED_SIZE = 77; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_sensor_response"; } + const char *message_name() const override { return "list_entities_sensor_response"; } #endif std::string unit_of_measurement{}; int32_t accuracy_decimals{0}; @@ -820,9 +877,9 @@ class ListEntitiesSensorResponse : public InfoResponseProtoMessage { class SensorStateResponse : public StateResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 25; - static constexpr uint16_t ESTIMATED_SIZE = 12; + static constexpr uint16_t ESTIMATED_SIZE = 16; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "sensor_state_response"; } + const char *message_name() const override { return "sensor_state_response"; } #endif float state{0.0f}; bool missing_state{false}; @@ -836,12 +893,14 @@ class SensorStateResponse : public StateResponseProtoMessage { bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; +#endif +#ifdef USE_SWITCH class ListEntitiesSwitchResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 17; - static constexpr uint16_t ESTIMATED_SIZE = 56; + static constexpr uint16_t ESTIMATED_SIZE = 60; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_switch_response"; } + const char *message_name() const override { return "list_entities_switch_response"; } #endif bool assumed_state{false}; std::string device_class{}; @@ -859,9 +918,9 @@ class ListEntitiesSwitchResponse : public InfoResponseProtoMessage { class SwitchStateResponse : public StateResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 26; - static constexpr uint16_t ESTIMATED_SIZE = 7; + static constexpr uint16_t ESTIMATED_SIZE = 11; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "switch_state_response"; } + const char *message_name() const override { return "switch_state_response"; } #endif bool state{false}; void encode(ProtoWriteBuffer buffer) const override; @@ -879,7 +938,7 @@ class SwitchCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 33; static constexpr uint16_t ESTIMATED_SIZE = 7; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "switch_command_request"; } + const char *message_name() const override { return "switch_command_request"; } #endif uint32_t key{0}; bool state{false}; @@ -893,12 +952,14 @@ class SwitchCommandRequest : public ProtoMessage { bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; +#endif +#ifdef USE_TEXT_SENSOR class ListEntitiesTextSensorResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 18; - static constexpr uint16_t ESTIMATED_SIZE = 54; + static constexpr uint16_t ESTIMATED_SIZE = 58; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_text_sensor_response"; } + const char *message_name() const override { return "list_entities_text_sensor_response"; } #endif std::string device_class{}; void encode(ProtoWriteBuffer buffer) const override; @@ -915,9 +976,9 @@ class ListEntitiesTextSensorResponse : public InfoResponseProtoMessage { class TextSensorStateResponse : public StateResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 27; - static constexpr uint16_t ESTIMATED_SIZE = 16; + static constexpr uint16_t ESTIMATED_SIZE = 20; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "text_sensor_state_response"; } + const char *message_name() const override { return "text_sensor_state_response"; } #endif std::string state{}; bool missing_state{false}; @@ -932,12 +993,13 @@ class TextSensorStateResponse : public StateResponseProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; +#endif class SubscribeLogsRequest : public ProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 28; static constexpr uint16_t ESTIMATED_SIZE = 4; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "subscribe_logs_request"; } + const char *message_name() const override { return "subscribe_logs_request"; } #endif enums::LogLevel level{}; bool dump_config{false}; @@ -955,7 +1017,7 @@ class SubscribeLogsResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 29; static constexpr uint16_t ESTIMATED_SIZE = 13; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "subscribe_logs_response"; } + const char *message_name() const override { return "subscribe_logs_response"; } #endif enums::LogLevel level{}; std::string message{}; @@ -970,12 +1032,13 @@ class SubscribeLogsResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; +#ifdef USE_API_NOISE class NoiseEncryptionSetKeyRequest : public ProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 124; static constexpr uint16_t ESTIMATED_SIZE = 9; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "noise_encryption_set_key_request"; } + const char *message_name() const override { return "noise_encryption_set_key_request"; } #endif std::string key{}; void encode(ProtoWriteBuffer buffer) const override; @@ -992,7 +1055,7 @@ class NoiseEncryptionSetKeyResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 125; static constexpr uint16_t ESTIMATED_SIZE = 2; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "noise_encryption_set_key_response"; } + const char *message_name() const override { return "noise_encryption_set_key_response"; } #endif bool success{false}; void encode(ProtoWriteBuffer buffer) const override; @@ -1004,15 +1067,14 @@ class NoiseEncryptionSetKeyResponse : public ProtoMessage { protected: bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; +#endif class SubscribeHomeassistantServicesRequest : public ProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 34; static constexpr uint16_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "subscribe_homeassistant_services_request"; } + const char *message_name() const override { return "subscribe_homeassistant_services_request"; } #endif - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1037,7 +1099,7 @@ class HomeassistantServiceResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 35; static constexpr uint16_t ESTIMATED_SIZE = 113; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "homeassistant_service_response"; } + const char *message_name() const override { return "homeassistant_service_response"; } #endif std::string service{}; std::vector data{}; @@ -1059,10 +1121,8 @@ class SubscribeHomeAssistantStatesRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 38; static constexpr uint16_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "subscribe_home_assistant_states_request"; } + const char *message_name() const override { return "subscribe_home_assistant_states_request"; } #endif - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1074,7 +1134,7 @@ class SubscribeHomeAssistantStateResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 39; static constexpr uint16_t ESTIMATED_SIZE = 20; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "subscribe_home_assistant_state_response"; } + const char *message_name() const override { return "subscribe_home_assistant_state_response"; } #endif std::string entity_id{}; std::string attribute{}; @@ -1094,7 +1154,7 @@ class HomeAssistantStateResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 40; static constexpr uint16_t ESTIMATED_SIZE = 27; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "home_assistant_state_response"; } + const char *message_name() const override { return "home_assistant_state_response"; } #endif std::string entity_id{}; std::string state{}; @@ -1113,10 +1173,8 @@ class GetTimeRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 36; static constexpr uint16_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "get_time_request"; } + const char *message_name() const override { return "get_time_request"; } #endif - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1128,7 +1186,7 @@ class GetTimeResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 37; static constexpr uint16_t ESTIMATED_SIZE = 5; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "get_time_response"; } + const char *message_name() const override { return "get_time_response"; } #endif uint32_t epoch_seconds{0}; void encode(ProtoWriteBuffer buffer) const override; @@ -1159,7 +1217,7 @@ class ListEntitiesServicesResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 41; static constexpr uint16_t ESTIMATED_SIZE = 48; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_services_response"; } + const char *message_name() const override { return "list_entities_services_response"; } #endif std::string name{}; uint32_t key{0}; @@ -1201,7 +1259,7 @@ class ExecuteServiceRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 42; static constexpr uint16_t ESTIMATED_SIZE = 39; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "execute_service_request"; } + const char *message_name() const override { return "execute_service_request"; } #endif uint32_t key{0}; std::vector args{}; @@ -1215,12 +1273,13 @@ class ExecuteServiceRequest : public ProtoMessage { bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; }; +#ifdef USE_CAMERA class ListEntitiesCameraResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 43; - static constexpr uint16_t ESTIMATED_SIZE = 45; + static constexpr uint16_t ESTIMATED_SIZE = 49; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_camera_response"; } + const char *message_name() const override { return "list_entities_camera_response"; } #endif void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; @@ -1238,7 +1297,7 @@ class CameraImageResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 44; static constexpr uint16_t ESTIMATED_SIZE = 16; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "camera_image_response"; } + const char *message_name() const override { return "camera_image_response"; } #endif uint32_t key{0}; std::string data{}; @@ -1259,7 +1318,7 @@ class CameraImageRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 45; static constexpr uint16_t ESTIMATED_SIZE = 4; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "camera_image_request"; } + const char *message_name() const override { return "camera_image_request"; } #endif bool single{false}; bool stream{false}; @@ -1272,12 +1331,14 @@ class CameraImageRequest : public ProtoMessage { protected: bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; +#endif +#ifdef USE_CLIMATE class ListEntitiesClimateResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 46; - static constexpr uint16_t ESTIMATED_SIZE = 151; + static constexpr uint16_t ESTIMATED_SIZE = 156; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_climate_response"; } + const char *message_name() const override { return "list_entities_climate_response"; } #endif bool supports_current_temperature{false}; bool supports_two_point_target_temperature{false}; @@ -1311,9 +1372,9 @@ class ListEntitiesClimateResponse : public InfoResponseProtoMessage { class ClimateStateResponse : public StateResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 47; - static constexpr uint16_t ESTIMATED_SIZE = 65; + static constexpr uint16_t ESTIMATED_SIZE = 70; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "climate_state_response"; } + const char *message_name() const override { return "climate_state_response"; } #endif enums::ClimateMode mode{}; float current_temperature{0.0f}; @@ -1345,7 +1406,7 @@ class ClimateCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 48; static constexpr uint16_t ESTIMATED_SIZE = 83; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "climate_command_request"; } + const char *message_name() const override { return "climate_command_request"; } #endif uint32_t key{0}; bool has_mode{false}; @@ -1381,12 +1442,14 @@ class ClimateCommandRequest : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; +#endif +#ifdef USE_NUMBER class ListEntitiesNumberResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 49; - static constexpr uint16_t ESTIMATED_SIZE = 80; + static constexpr uint16_t ESTIMATED_SIZE = 84; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_number_response"; } + const char *message_name() const override { return "list_entities_number_response"; } #endif float min_value{0.0f}; float max_value{0.0f}; @@ -1408,9 +1471,9 @@ class ListEntitiesNumberResponse : public InfoResponseProtoMessage { class NumberStateResponse : public StateResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 50; - static constexpr uint16_t ESTIMATED_SIZE = 12; + static constexpr uint16_t ESTIMATED_SIZE = 16; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "number_state_response"; } + const char *message_name() const override { return "number_state_response"; } #endif float state{0.0f}; bool missing_state{false}; @@ -1429,7 +1492,7 @@ class NumberCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 51; static constexpr uint16_t ESTIMATED_SIZE = 10; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "number_command_request"; } + const char *message_name() const override { return "number_command_request"; } #endif uint32_t key{0}; float state{0.0f}; @@ -1442,12 +1505,14 @@ class NumberCommandRequest : public ProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; }; +#endif +#ifdef USE_SELECT class ListEntitiesSelectResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 52; - static constexpr uint16_t ESTIMATED_SIZE = 63; + static constexpr uint16_t ESTIMATED_SIZE = 67; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_select_response"; } + const char *message_name() const override { return "list_entities_select_response"; } #endif std::vector options{}; void encode(ProtoWriteBuffer buffer) const override; @@ -1464,9 +1529,9 @@ class ListEntitiesSelectResponse : public InfoResponseProtoMessage { class SelectStateResponse : public StateResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 53; - static constexpr uint16_t ESTIMATED_SIZE = 16; + static constexpr uint16_t ESTIMATED_SIZE = 20; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "select_state_response"; } + const char *message_name() const override { return "select_state_response"; } #endif std::string state{}; bool missing_state{false}; @@ -1486,7 +1551,7 @@ class SelectCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 54; static constexpr uint16_t ESTIMATED_SIZE = 14; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "select_command_request"; } + const char *message_name() const override { return "select_command_request"; } #endif uint32_t key{0}; std::string state{}; @@ -1500,12 +1565,14 @@ class SelectCommandRequest : public ProtoMessage { bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; }; +#endif +#ifdef USE_SIREN class ListEntitiesSirenResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 55; - static constexpr uint16_t ESTIMATED_SIZE = 67; + static constexpr uint16_t ESTIMATED_SIZE = 71; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_siren_response"; } + const char *message_name() const override { return "list_entities_siren_response"; } #endif std::vector tones{}; bool supports_duration{false}; @@ -1524,9 +1591,9 @@ class ListEntitiesSirenResponse : public InfoResponseProtoMessage { class SirenStateResponse : public StateResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 56; - static constexpr uint16_t ESTIMATED_SIZE = 7; + static constexpr uint16_t ESTIMATED_SIZE = 11; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "siren_state_response"; } + const char *message_name() const override { return "siren_state_response"; } #endif bool state{false}; void encode(ProtoWriteBuffer buffer) const override; @@ -1544,7 +1611,7 @@ class SirenCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 57; static constexpr uint16_t ESTIMATED_SIZE = 33; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "siren_command_request"; } + const char *message_name() const override { return "siren_command_request"; } #endif uint32_t key{0}; bool has_state{false}; @@ -1566,12 +1633,14 @@ class SirenCommandRequest : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; +#endif +#ifdef USE_LOCK class ListEntitiesLockResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 58; - static constexpr uint16_t ESTIMATED_SIZE = 60; + static constexpr uint16_t ESTIMATED_SIZE = 64; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_lock_response"; } + const char *message_name() const override { return "list_entities_lock_response"; } #endif bool assumed_state{false}; bool supports_open{false}; @@ -1591,9 +1660,9 @@ class ListEntitiesLockResponse : public InfoResponseProtoMessage { class LockStateResponse : public StateResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 59; - static constexpr uint16_t ESTIMATED_SIZE = 7; + static constexpr uint16_t ESTIMATED_SIZE = 11; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "lock_state_response"; } + const char *message_name() const override { return "lock_state_response"; } #endif enums::LockState state{}; void encode(ProtoWriteBuffer buffer) const override; @@ -1611,7 +1680,7 @@ class LockCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 60; static constexpr uint16_t ESTIMATED_SIZE = 18; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "lock_command_request"; } + const char *message_name() const override { return "lock_command_request"; } #endif uint32_t key{0}; enums::LockCommand command{}; @@ -1628,12 +1697,14 @@ class LockCommandRequest : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; +#endif +#ifdef USE_BUTTON class ListEntitiesButtonResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 61; - static constexpr uint16_t ESTIMATED_SIZE = 54; + static constexpr uint16_t ESTIMATED_SIZE = 58; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_button_response"; } + const char *message_name() const override { return "list_entities_button_response"; } #endif std::string device_class{}; void encode(ProtoWriteBuffer buffer) const override; @@ -1652,7 +1723,7 @@ class ButtonCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 62; static constexpr uint16_t ESTIMATED_SIZE = 5; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "button_command_request"; } + const char *message_name() const override { return "button_command_request"; } #endif uint32_t key{0}; void encode(ProtoWriteBuffer buffer) const override; @@ -1664,6 +1735,8 @@ class ButtonCommandRequest : public ProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; }; +#endif +#ifdef USE_MEDIA_PLAYER class MediaPlayerSupportedFormat : public ProtoMessage { public: std::string format{}; @@ -1684,9 +1757,9 @@ class MediaPlayerSupportedFormat : public ProtoMessage { class ListEntitiesMediaPlayerResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 63; - static constexpr uint16_t ESTIMATED_SIZE = 81; + static constexpr uint16_t ESTIMATED_SIZE = 85; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_media_player_response"; } + const char *message_name() const override { return "list_entities_media_player_response"; } #endif bool supports_pause{false}; std::vector supported_formats{}; @@ -1704,9 +1777,9 @@ class ListEntitiesMediaPlayerResponse : public InfoResponseProtoMessage { class MediaPlayerStateResponse : public StateResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 64; - static constexpr uint16_t ESTIMATED_SIZE = 14; + static constexpr uint16_t ESTIMATED_SIZE = 18; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "media_player_state_response"; } + const char *message_name() const override { return "media_player_state_response"; } #endif enums::MediaPlayerState state{}; float volume{0.0f}; @@ -1726,7 +1799,7 @@ class MediaPlayerCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 65; static constexpr uint16_t ESTIMATED_SIZE = 31; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "media_player_command_request"; } + const char *message_name() const override { return "media_player_command_request"; } #endif uint32_t key{0}; bool has_command{false}; @@ -1748,12 +1821,14 @@ class MediaPlayerCommandRequest : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; +#endif +#ifdef USE_BLUETOOTH_PROXY class SubscribeBluetoothLEAdvertisementsRequest : public ProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 66; static constexpr uint16_t ESTIMATED_SIZE = 4; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "subscribe_bluetooth_le_advertisements_request"; } + const char *message_name() const override { return "subscribe_bluetooth_le_advertisements_request"; } #endif uint32_t flags{0}; void encode(ProtoWriteBuffer buffer) const override; @@ -1785,7 +1860,7 @@ class BluetoothLEAdvertisementResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 67; static constexpr uint16_t ESTIMATED_SIZE = 107; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_le_advertisement_response"; } + const char *message_name() const override { return "bluetooth_le_advertisement_response"; } #endif uint64_t address{0}; std::string name{}; @@ -1825,7 +1900,7 @@ class BluetoothLERawAdvertisementsResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 93; static constexpr uint16_t ESTIMATED_SIZE = 34; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_le_raw_advertisements_response"; } + const char *message_name() const override { return "bluetooth_le_raw_advertisements_response"; } #endif std::vector advertisements{}; void encode(ProtoWriteBuffer buffer) const override; @@ -1842,7 +1917,7 @@ class BluetoothDeviceRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 68; static constexpr uint16_t ESTIMATED_SIZE = 12; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_device_request"; } + const char *message_name() const override { return "bluetooth_device_request"; } #endif uint64_t address{0}; enums::BluetoothDeviceRequestType request_type{}; @@ -1862,7 +1937,7 @@ class BluetoothDeviceConnectionResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 69; static constexpr uint16_t ESTIMATED_SIZE = 14; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_device_connection_response"; } + const char *message_name() const override { return "bluetooth_device_connection_response"; } #endif uint64_t address{0}; bool connected{false}; @@ -1882,7 +1957,7 @@ class BluetoothGATTGetServicesRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 70; static constexpr uint16_t ESTIMATED_SIZE = 4; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_gatt_get_services_request"; } + const char *message_name() const override { return "bluetooth_gatt_get_services_request"; } #endif uint64_t address{0}; void encode(ProtoWriteBuffer buffer) const override; @@ -1943,7 +2018,7 @@ class BluetoothGATTGetServicesResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 71; static constexpr uint16_t ESTIMATED_SIZE = 38; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_gatt_get_services_response"; } + const char *message_name() const override { return "bluetooth_gatt_get_services_response"; } #endif uint64_t address{0}; std::vector services{}; @@ -1962,7 +2037,7 @@ class BluetoothGATTGetServicesDoneResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 72; static constexpr uint16_t ESTIMATED_SIZE = 4; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_gatt_get_services_done_response"; } + const char *message_name() const override { return "bluetooth_gatt_get_services_done_response"; } #endif uint64_t address{0}; void encode(ProtoWriteBuffer buffer) const override; @@ -1979,7 +2054,7 @@ class BluetoothGATTReadRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 73; static constexpr uint16_t ESTIMATED_SIZE = 8; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_gatt_read_request"; } + const char *message_name() const override { return "bluetooth_gatt_read_request"; } #endif uint64_t address{0}; uint32_t handle{0}; @@ -1997,7 +2072,7 @@ class BluetoothGATTReadResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 74; static constexpr uint16_t ESTIMATED_SIZE = 17; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_gatt_read_response"; } + const char *message_name() const override { return "bluetooth_gatt_read_response"; } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2017,7 +2092,7 @@ class BluetoothGATTWriteRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 75; static constexpr uint16_t ESTIMATED_SIZE = 19; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_gatt_write_request"; } + const char *message_name() const override { return "bluetooth_gatt_write_request"; } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2038,7 +2113,7 @@ class BluetoothGATTReadDescriptorRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 76; static constexpr uint16_t ESTIMATED_SIZE = 8; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_gatt_read_descriptor_request"; } + const char *message_name() const override { return "bluetooth_gatt_read_descriptor_request"; } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2056,7 +2131,7 @@ class BluetoothGATTWriteDescriptorRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 77; static constexpr uint16_t ESTIMATED_SIZE = 17; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_gatt_write_descriptor_request"; } + const char *message_name() const override { return "bluetooth_gatt_write_descriptor_request"; } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2076,7 +2151,7 @@ class BluetoothGATTNotifyRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 78; static constexpr uint16_t ESTIMATED_SIZE = 10; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_gatt_notify_request"; } + const char *message_name() const override { return "bluetooth_gatt_notify_request"; } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2095,7 +2170,7 @@ class BluetoothGATTNotifyDataResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 79; static constexpr uint16_t ESTIMATED_SIZE = 17; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_gatt_notify_data_response"; } + const char *message_name() const override { return "bluetooth_gatt_notify_data_response"; } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2115,10 +2190,8 @@ class SubscribeBluetoothConnectionsFreeRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 80; static constexpr uint16_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "subscribe_bluetooth_connections_free_request"; } + const char *message_name() const override { return "subscribe_bluetooth_connections_free_request"; } #endif - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2130,7 +2203,7 @@ class BluetoothConnectionsFreeResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 81; static constexpr uint16_t ESTIMATED_SIZE = 16; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_connections_free_response"; } + const char *message_name() const override { return "bluetooth_connections_free_response"; } #endif uint32_t free{0}; uint32_t limit{0}; @@ -2149,7 +2222,7 @@ class BluetoothGATTErrorResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 82; static constexpr uint16_t ESTIMATED_SIZE = 12; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_gatt_error_response"; } + const char *message_name() const override { return "bluetooth_gatt_error_response"; } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2168,7 +2241,7 @@ class BluetoothGATTWriteResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 83; static constexpr uint16_t ESTIMATED_SIZE = 8; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_gatt_write_response"; } + const char *message_name() const override { return "bluetooth_gatt_write_response"; } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2186,7 +2259,7 @@ class BluetoothGATTNotifyResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 84; static constexpr uint16_t ESTIMATED_SIZE = 8; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_gatt_notify_response"; } + const char *message_name() const override { return "bluetooth_gatt_notify_response"; } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2204,7 +2277,7 @@ class BluetoothDevicePairingResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 85; static constexpr uint16_t ESTIMATED_SIZE = 10; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_device_pairing_response"; } + const char *message_name() const override { return "bluetooth_device_pairing_response"; } #endif uint64_t address{0}; bool paired{false}; @@ -2223,7 +2296,7 @@ class BluetoothDeviceUnpairingResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 86; static constexpr uint16_t ESTIMATED_SIZE = 10; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_device_unpairing_response"; } + const char *message_name() const override { return "bluetooth_device_unpairing_response"; } #endif uint64_t address{0}; bool success{false}; @@ -2242,10 +2315,8 @@ class UnsubscribeBluetoothLEAdvertisementsRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 87; static constexpr uint16_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "unsubscribe_bluetooth_le_advertisements_request"; } + const char *message_name() const override { return "unsubscribe_bluetooth_le_advertisements_request"; } #endif - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2257,7 +2328,7 @@ class BluetoothDeviceClearCacheResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 88; static constexpr uint16_t ESTIMATED_SIZE = 10; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_device_clear_cache_response"; } + const char *message_name() const override { return "bluetooth_device_clear_cache_response"; } #endif uint64_t address{0}; bool success{false}; @@ -2276,7 +2347,7 @@ class BluetoothScannerStateResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 126; static constexpr uint16_t ESTIMATED_SIZE = 4; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_scanner_state_response"; } + const char *message_name() const override { return "bluetooth_scanner_state_response"; } #endif enums::BluetoothScannerState state{}; enums::BluetoothScannerMode mode{}; @@ -2294,7 +2365,7 @@ class BluetoothScannerSetModeRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 127; static constexpr uint16_t ESTIMATED_SIZE = 2; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "bluetooth_scanner_set_mode_request"; } + const char *message_name() const override { return "bluetooth_scanner_set_mode_request"; } #endif enums::BluetoothScannerMode mode{}; void encode(ProtoWriteBuffer buffer) const override; @@ -2306,12 +2377,14 @@ class BluetoothScannerSetModeRequest : public ProtoMessage { protected: bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; +#endif +#ifdef USE_VOICE_ASSISTANT class SubscribeVoiceAssistantRequest : public ProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 89; static constexpr uint16_t ESTIMATED_SIZE = 6; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "subscribe_voice_assistant_request"; } + const char *message_name() const override { return "subscribe_voice_assistant_request"; } #endif bool subscribe{false}; uint32_t flags{0}; @@ -2344,7 +2417,7 @@ class VoiceAssistantRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 90; static constexpr uint16_t ESTIMATED_SIZE = 41; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "voice_assistant_request"; } + const char *message_name() const override { return "voice_assistant_request"; } #endif bool start{false}; std::string conversation_id{}; @@ -2366,7 +2439,7 @@ class VoiceAssistantResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 91; static constexpr uint16_t ESTIMATED_SIZE = 6; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "voice_assistant_response"; } + const char *message_name() const override { return "voice_assistant_response"; } #endif uint32_t port{0}; bool error{false}; @@ -2397,7 +2470,7 @@ class VoiceAssistantEventResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 92; static constexpr uint16_t ESTIMATED_SIZE = 36; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "voice_assistant_event_response"; } + const char *message_name() const override { return "voice_assistant_event_response"; } #endif enums::VoiceAssistantEvent event_type{}; std::vector data{}; @@ -2416,7 +2489,7 @@ class VoiceAssistantAudio : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 106; static constexpr uint16_t ESTIMATED_SIZE = 11; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "voice_assistant_audio"; } + const char *message_name() const override { return "voice_assistant_audio"; } #endif std::string data{}; bool end{false}; @@ -2435,7 +2508,7 @@ class VoiceAssistantTimerEventResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 115; static constexpr uint16_t ESTIMATED_SIZE = 30; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "voice_assistant_timer_event_response"; } + const char *message_name() const override { return "voice_assistant_timer_event_response"; } #endif enums::VoiceAssistantTimerEvent event_type{}; std::string timer_id{}; @@ -2458,7 +2531,7 @@ class VoiceAssistantAnnounceRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 119; static constexpr uint16_t ESTIMATED_SIZE = 29; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "voice_assistant_announce_request"; } + const char *message_name() const override { return "voice_assistant_announce_request"; } #endif std::string media_id{}; std::string text{}; @@ -2479,7 +2552,7 @@ class VoiceAssistantAnnounceFinished : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 120; static constexpr uint16_t ESTIMATED_SIZE = 2; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "voice_assistant_announce_finished"; } + const char *message_name() const override { return "voice_assistant_announce_finished"; } #endif bool success{false}; void encode(ProtoWriteBuffer buffer) const override; @@ -2510,10 +2583,8 @@ class VoiceAssistantConfigurationRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 121; static constexpr uint16_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "voice_assistant_configuration_request"; } + const char *message_name() const override { return "voice_assistant_configuration_request"; } #endif - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2525,7 +2596,7 @@ class VoiceAssistantConfigurationResponse : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 122; static constexpr uint16_t ESTIMATED_SIZE = 56; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "voice_assistant_configuration_response"; } + const char *message_name() const override { return "voice_assistant_configuration_response"; } #endif std::vector available_wake_words{}; std::vector active_wake_words{}; @@ -2545,7 +2616,7 @@ class VoiceAssistantSetConfiguration : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 123; static constexpr uint16_t ESTIMATED_SIZE = 18; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "voice_assistant_set_configuration"; } + const char *message_name() const override { return "voice_assistant_set_configuration"; } #endif std::vector active_wake_words{}; void encode(ProtoWriteBuffer buffer) const override; @@ -2557,12 +2628,14 @@ class VoiceAssistantSetConfiguration : public ProtoMessage { protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; }; +#endif +#ifdef USE_ALARM_CONTROL_PANEL class ListEntitiesAlarmControlPanelResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 94; - static constexpr uint16_t ESTIMATED_SIZE = 53; + static constexpr uint16_t ESTIMATED_SIZE = 57; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_alarm_control_panel_response"; } + const char *message_name() const override { return "list_entities_alarm_control_panel_response"; } #endif uint32_t supported_features{0}; bool requires_code{false}; @@ -2581,9 +2654,9 @@ class ListEntitiesAlarmControlPanelResponse : public InfoResponseProtoMessage { class AlarmControlPanelStateResponse : public StateResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 95; - static constexpr uint16_t ESTIMATED_SIZE = 7; + static constexpr uint16_t ESTIMATED_SIZE = 11; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "alarm_control_panel_state_response"; } + const char *message_name() const override { return "alarm_control_panel_state_response"; } #endif enums::AlarmControlPanelState state{}; void encode(ProtoWriteBuffer buffer) const override; @@ -2601,7 +2674,7 @@ class AlarmControlPanelCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 96; static constexpr uint16_t ESTIMATED_SIZE = 16; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "alarm_control_panel_command_request"; } + const char *message_name() const override { return "alarm_control_panel_command_request"; } #endif uint32_t key{0}; enums::AlarmControlPanelStateCommand command{}; @@ -2617,12 +2690,14 @@ class AlarmControlPanelCommandRequest : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; +#endif +#ifdef USE_TEXT class ListEntitiesTextResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 97; - static constexpr uint16_t ESTIMATED_SIZE = 64; + static constexpr uint16_t ESTIMATED_SIZE = 68; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_text_response"; } + const char *message_name() const override { return "list_entities_text_response"; } #endif uint32_t min_length{0}; uint32_t max_length{0}; @@ -2642,9 +2717,9 @@ class ListEntitiesTextResponse : public InfoResponseProtoMessage { class TextStateResponse : public StateResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 98; - static constexpr uint16_t ESTIMATED_SIZE = 16; + static constexpr uint16_t ESTIMATED_SIZE = 20; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "text_state_response"; } + const char *message_name() const override { return "text_state_response"; } #endif std::string state{}; bool missing_state{false}; @@ -2664,7 +2739,7 @@ class TextCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 99; static constexpr uint16_t ESTIMATED_SIZE = 14; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "text_command_request"; } + const char *message_name() const override { return "text_command_request"; } #endif uint32_t key{0}; std::string state{}; @@ -2678,12 +2753,14 @@ class TextCommandRequest : public ProtoMessage { bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; }; +#endif +#ifdef USE_DATETIME_DATE class ListEntitiesDateResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 100; - static constexpr uint16_t ESTIMATED_SIZE = 45; + static constexpr uint16_t ESTIMATED_SIZE = 49; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_date_response"; } + const char *message_name() const override { return "list_entities_date_response"; } #endif void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; @@ -2699,9 +2776,9 @@ class ListEntitiesDateResponse : public InfoResponseProtoMessage { class DateStateResponse : public StateResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 101; - static constexpr uint16_t ESTIMATED_SIZE = 19; + static constexpr uint16_t ESTIMATED_SIZE = 23; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "date_state_response"; } + const char *message_name() const override { return "date_state_response"; } #endif bool missing_state{false}; uint32_t year{0}; @@ -2722,7 +2799,7 @@ class DateCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 102; static constexpr uint16_t ESTIMATED_SIZE = 17; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "date_command_request"; } + const char *message_name() const override { return "date_command_request"; } #endif uint32_t key{0}; uint32_t year{0}; @@ -2738,12 +2815,14 @@ class DateCommandRequest : public ProtoMessage { bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; +#endif +#ifdef USE_DATETIME_TIME class ListEntitiesTimeResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 103; - static constexpr uint16_t ESTIMATED_SIZE = 45; + static constexpr uint16_t ESTIMATED_SIZE = 49; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_time_response"; } + const char *message_name() const override { return "list_entities_time_response"; } #endif void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; @@ -2759,9 +2838,9 @@ class ListEntitiesTimeResponse : public InfoResponseProtoMessage { class TimeStateResponse : public StateResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 104; - static constexpr uint16_t ESTIMATED_SIZE = 19; + static constexpr uint16_t ESTIMATED_SIZE = 23; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "time_state_response"; } + const char *message_name() const override { return "time_state_response"; } #endif bool missing_state{false}; uint32_t hour{0}; @@ -2782,7 +2861,7 @@ class TimeCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 105; static constexpr uint16_t ESTIMATED_SIZE = 17; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "time_command_request"; } + const char *message_name() const override { return "time_command_request"; } #endif uint32_t key{0}; uint32_t hour{0}; @@ -2798,12 +2877,14 @@ class TimeCommandRequest : public ProtoMessage { bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; +#endif +#ifdef USE_EVENT class ListEntitiesEventResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 107; - static constexpr uint16_t ESTIMATED_SIZE = 72; + static constexpr uint16_t ESTIMATED_SIZE = 76; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_event_response"; } + const char *message_name() const override { return "list_entities_event_response"; } #endif std::string device_class{}; std::vector event_types{}; @@ -2821,9 +2902,9 @@ class ListEntitiesEventResponse : public InfoResponseProtoMessage { class EventResponse : public StateResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 108; - static constexpr uint16_t ESTIMATED_SIZE = 14; + static constexpr uint16_t ESTIMATED_SIZE = 18; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "event_response"; } + const char *message_name() const override { return "event_response"; } #endif std::string event_type{}; void encode(ProtoWriteBuffer buffer) const override; @@ -2835,13 +2916,16 @@ class EventResponse : public StateResponseProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; +#endif +#ifdef USE_VALVE class ListEntitiesValveResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 109; - static constexpr uint16_t ESTIMATED_SIZE = 60; + static constexpr uint16_t ESTIMATED_SIZE = 64; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_valve_response"; } + const char *message_name() const override { return "list_entities_valve_response"; } #endif std::string device_class{}; bool assumed_state{false}; @@ -2861,9 +2945,9 @@ class ListEntitiesValveResponse : public InfoResponseProtoMessage { class ValveStateResponse : public StateResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 110; - static constexpr uint16_t ESTIMATED_SIZE = 12; + static constexpr uint16_t ESTIMATED_SIZE = 16; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "valve_state_response"; } + const char *message_name() const override { return "valve_state_response"; } #endif float position{0.0f}; enums::ValveOperation current_operation{}; @@ -2882,7 +2966,7 @@ class ValveCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 111; static constexpr uint16_t ESTIMATED_SIZE = 14; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "valve_command_request"; } + const char *message_name() const override { return "valve_command_request"; } #endif uint32_t key{0}; bool has_position{false}; @@ -2898,12 +2982,14 @@ class ValveCommandRequest : public ProtoMessage { bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; +#endif +#ifdef USE_DATETIME_DATETIME class ListEntitiesDateTimeResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 112; - static constexpr uint16_t ESTIMATED_SIZE = 45; + static constexpr uint16_t ESTIMATED_SIZE = 49; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_date_time_response"; } + const char *message_name() const override { return "list_entities_date_time_response"; } #endif void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; @@ -2919,9 +3005,9 @@ class ListEntitiesDateTimeResponse : public InfoResponseProtoMessage { class DateTimeStateResponse : public StateResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 113; - static constexpr uint16_t ESTIMATED_SIZE = 12; + static constexpr uint16_t ESTIMATED_SIZE = 16; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "date_time_state_response"; } + const char *message_name() const override { return "date_time_state_response"; } #endif bool missing_state{false}; uint32_t epoch_seconds{0}; @@ -2940,7 +3026,7 @@ class DateTimeCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 114; static constexpr uint16_t ESTIMATED_SIZE = 10; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "date_time_command_request"; } + const char *message_name() const override { return "date_time_command_request"; } #endif uint32_t key{0}; uint32_t epoch_seconds{0}; @@ -2953,12 +3039,14 @@ class DateTimeCommandRequest : public ProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; }; +#endif +#ifdef USE_UPDATE class ListEntitiesUpdateResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 116; - static constexpr uint16_t ESTIMATED_SIZE = 54; + static constexpr uint16_t ESTIMATED_SIZE = 58; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "list_entities_update_response"; } + const char *message_name() const override { return "list_entities_update_response"; } #endif std::string device_class{}; void encode(ProtoWriteBuffer buffer) const override; @@ -2975,9 +3063,9 @@ class ListEntitiesUpdateResponse : public InfoResponseProtoMessage { class UpdateStateResponse : public StateResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 117; - static constexpr uint16_t ESTIMATED_SIZE = 61; + static constexpr uint16_t ESTIMATED_SIZE = 65; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "update_state_response"; } + const char *message_name() const override { return "update_state_response"; } #endif bool missing_state{false}; bool in_progress{false}; @@ -3004,7 +3092,7 @@ class UpdateCommandRequest : public ProtoMessage { static constexpr uint16_t MESSAGE_TYPE = 118; static constexpr uint16_t ESTIMATED_SIZE = 7; #ifdef HAS_PROTO_MESSAGE_DUMP - static constexpr const char *message_name() { return "update_command_request"; } + const char *message_name() const override { return "update_command_request"; } #endif uint32_t key{0}; enums::UpdateCommand command{}; @@ -3018,6 +3106,7 @@ class UpdateCommandRequest : public ProtoMessage { bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; +#endif } // namespace api } // namespace esphome diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp new file mode 100644 index 0000000000..48ddd42d61 --- /dev/null +++ b/esphome/components/api/api_pb2_dump.cpp @@ -0,0 +1,4333 @@ +// This file was automatically generated with a tool. +// See script/api_protobuf/api_protobuf.py +#include "api_pb2.h" +#include "esphome/core/helpers.h" + +#include + +#ifdef HAS_PROTO_MESSAGE_DUMP + +namespace esphome { +namespace api { + +template<> const char *proto_enum_to_string(enums::EntityCategory value) { + switch (value) { + case enums::ENTITY_CATEGORY_NONE: + return "ENTITY_CATEGORY_NONE"; + case enums::ENTITY_CATEGORY_CONFIG: + return "ENTITY_CATEGORY_CONFIG"; + case enums::ENTITY_CATEGORY_DIAGNOSTIC: + return "ENTITY_CATEGORY_DIAGNOSTIC"; + default: + return "UNKNOWN"; + } +} +#ifdef USE_COVER +template<> const char *proto_enum_to_string(enums::LegacyCoverState value) { + switch (value) { + case enums::LEGACY_COVER_STATE_OPEN: + return "LEGACY_COVER_STATE_OPEN"; + case enums::LEGACY_COVER_STATE_CLOSED: + return "LEGACY_COVER_STATE_CLOSED"; + default: + return "UNKNOWN"; + } +} +template<> const char *proto_enum_to_string(enums::CoverOperation value) { + switch (value) { + case enums::COVER_OPERATION_IDLE: + return "COVER_OPERATION_IDLE"; + case enums::COVER_OPERATION_IS_OPENING: + return "COVER_OPERATION_IS_OPENING"; + case enums::COVER_OPERATION_IS_CLOSING: + return "COVER_OPERATION_IS_CLOSING"; + default: + return "UNKNOWN"; + } +} +template<> const char *proto_enum_to_string(enums::LegacyCoverCommand value) { + switch (value) { + case enums::LEGACY_COVER_COMMAND_OPEN: + return "LEGACY_COVER_COMMAND_OPEN"; + case enums::LEGACY_COVER_COMMAND_CLOSE: + return "LEGACY_COVER_COMMAND_CLOSE"; + case enums::LEGACY_COVER_COMMAND_STOP: + return "LEGACY_COVER_COMMAND_STOP"; + default: + return "UNKNOWN"; + } +} +#endif +#ifdef USE_FAN +template<> const char *proto_enum_to_string(enums::FanSpeed value) { + switch (value) { + case enums::FAN_SPEED_LOW: + return "FAN_SPEED_LOW"; + case enums::FAN_SPEED_MEDIUM: + return "FAN_SPEED_MEDIUM"; + case enums::FAN_SPEED_HIGH: + return "FAN_SPEED_HIGH"; + default: + return "UNKNOWN"; + } +} +template<> const char *proto_enum_to_string(enums::FanDirection value) { + switch (value) { + case enums::FAN_DIRECTION_FORWARD: + return "FAN_DIRECTION_FORWARD"; + case enums::FAN_DIRECTION_REVERSE: + return "FAN_DIRECTION_REVERSE"; + default: + return "UNKNOWN"; + } +} +#endif +#ifdef USE_LIGHT +template<> const char *proto_enum_to_string(enums::ColorMode value) { + switch (value) { + case enums::COLOR_MODE_UNKNOWN: + return "COLOR_MODE_UNKNOWN"; + case enums::COLOR_MODE_ON_OFF: + return "COLOR_MODE_ON_OFF"; + case enums::COLOR_MODE_LEGACY_BRIGHTNESS: + return "COLOR_MODE_LEGACY_BRIGHTNESS"; + case enums::COLOR_MODE_BRIGHTNESS: + return "COLOR_MODE_BRIGHTNESS"; + case enums::COLOR_MODE_WHITE: + return "COLOR_MODE_WHITE"; + case enums::COLOR_MODE_COLOR_TEMPERATURE: + return "COLOR_MODE_COLOR_TEMPERATURE"; + case enums::COLOR_MODE_COLD_WARM_WHITE: + return "COLOR_MODE_COLD_WARM_WHITE"; + case enums::COLOR_MODE_RGB: + return "COLOR_MODE_RGB"; + case enums::COLOR_MODE_RGB_WHITE: + return "COLOR_MODE_RGB_WHITE"; + case enums::COLOR_MODE_RGB_COLOR_TEMPERATURE: + return "COLOR_MODE_RGB_COLOR_TEMPERATURE"; + case enums::COLOR_MODE_RGB_COLD_WARM_WHITE: + return "COLOR_MODE_RGB_COLD_WARM_WHITE"; + default: + return "UNKNOWN"; + } +} +#endif +#ifdef USE_SENSOR +template<> const char *proto_enum_to_string(enums::SensorStateClass value) { + switch (value) { + case enums::STATE_CLASS_NONE: + return "STATE_CLASS_NONE"; + case enums::STATE_CLASS_MEASUREMENT: + return "STATE_CLASS_MEASUREMENT"; + case enums::STATE_CLASS_TOTAL_INCREASING: + return "STATE_CLASS_TOTAL_INCREASING"; + case enums::STATE_CLASS_TOTAL: + return "STATE_CLASS_TOTAL"; + default: + return "UNKNOWN"; + } +} +template<> const char *proto_enum_to_string(enums::SensorLastResetType value) { + switch (value) { + case enums::LAST_RESET_NONE: + return "LAST_RESET_NONE"; + case enums::LAST_RESET_NEVER: + return "LAST_RESET_NEVER"; + case enums::LAST_RESET_AUTO: + return "LAST_RESET_AUTO"; + default: + return "UNKNOWN"; + } +} +#endif +template<> const char *proto_enum_to_string(enums::LogLevel value) { + switch (value) { + case enums::LOG_LEVEL_NONE: + return "LOG_LEVEL_NONE"; + case enums::LOG_LEVEL_ERROR: + return "LOG_LEVEL_ERROR"; + case enums::LOG_LEVEL_WARN: + return "LOG_LEVEL_WARN"; + case enums::LOG_LEVEL_INFO: + return "LOG_LEVEL_INFO"; + case enums::LOG_LEVEL_CONFIG: + return "LOG_LEVEL_CONFIG"; + case enums::LOG_LEVEL_DEBUG: + return "LOG_LEVEL_DEBUG"; + case enums::LOG_LEVEL_VERBOSE: + return "LOG_LEVEL_VERBOSE"; + case enums::LOG_LEVEL_VERY_VERBOSE: + return "LOG_LEVEL_VERY_VERBOSE"; + default: + return "UNKNOWN"; + } +} +template<> const char *proto_enum_to_string(enums::ServiceArgType value) { + switch (value) { + case enums::SERVICE_ARG_TYPE_BOOL: + return "SERVICE_ARG_TYPE_BOOL"; + case enums::SERVICE_ARG_TYPE_INT: + return "SERVICE_ARG_TYPE_INT"; + case enums::SERVICE_ARG_TYPE_FLOAT: + return "SERVICE_ARG_TYPE_FLOAT"; + case enums::SERVICE_ARG_TYPE_STRING: + return "SERVICE_ARG_TYPE_STRING"; + case enums::SERVICE_ARG_TYPE_BOOL_ARRAY: + return "SERVICE_ARG_TYPE_BOOL_ARRAY"; + case enums::SERVICE_ARG_TYPE_INT_ARRAY: + return "SERVICE_ARG_TYPE_INT_ARRAY"; + case enums::SERVICE_ARG_TYPE_FLOAT_ARRAY: + return "SERVICE_ARG_TYPE_FLOAT_ARRAY"; + case enums::SERVICE_ARG_TYPE_STRING_ARRAY: + return "SERVICE_ARG_TYPE_STRING_ARRAY"; + default: + return "UNKNOWN"; + } +} +#ifdef USE_CLIMATE +template<> const char *proto_enum_to_string(enums::ClimateMode value) { + switch (value) { + case enums::CLIMATE_MODE_OFF: + return "CLIMATE_MODE_OFF"; + case enums::CLIMATE_MODE_HEAT_COOL: + return "CLIMATE_MODE_HEAT_COOL"; + case enums::CLIMATE_MODE_COOL: + return "CLIMATE_MODE_COOL"; + case enums::CLIMATE_MODE_HEAT: + return "CLIMATE_MODE_HEAT"; + case enums::CLIMATE_MODE_FAN_ONLY: + return "CLIMATE_MODE_FAN_ONLY"; + case enums::CLIMATE_MODE_DRY: + return "CLIMATE_MODE_DRY"; + case enums::CLIMATE_MODE_AUTO: + return "CLIMATE_MODE_AUTO"; + default: + return "UNKNOWN"; + } +} +template<> const char *proto_enum_to_string(enums::ClimateFanMode value) { + switch (value) { + case enums::CLIMATE_FAN_ON: + return "CLIMATE_FAN_ON"; + case enums::CLIMATE_FAN_OFF: + return "CLIMATE_FAN_OFF"; + case enums::CLIMATE_FAN_AUTO: + return "CLIMATE_FAN_AUTO"; + case enums::CLIMATE_FAN_LOW: + return "CLIMATE_FAN_LOW"; + case enums::CLIMATE_FAN_MEDIUM: + return "CLIMATE_FAN_MEDIUM"; + case enums::CLIMATE_FAN_HIGH: + return "CLIMATE_FAN_HIGH"; + case enums::CLIMATE_FAN_MIDDLE: + return "CLIMATE_FAN_MIDDLE"; + case enums::CLIMATE_FAN_FOCUS: + return "CLIMATE_FAN_FOCUS"; + case enums::CLIMATE_FAN_DIFFUSE: + return "CLIMATE_FAN_DIFFUSE"; + case enums::CLIMATE_FAN_QUIET: + return "CLIMATE_FAN_QUIET"; + default: + return "UNKNOWN"; + } +} +template<> const char *proto_enum_to_string(enums::ClimateSwingMode value) { + switch (value) { + case enums::CLIMATE_SWING_OFF: + return "CLIMATE_SWING_OFF"; + case enums::CLIMATE_SWING_BOTH: + return "CLIMATE_SWING_BOTH"; + case enums::CLIMATE_SWING_VERTICAL: + return "CLIMATE_SWING_VERTICAL"; + case enums::CLIMATE_SWING_HORIZONTAL: + return "CLIMATE_SWING_HORIZONTAL"; + default: + return "UNKNOWN"; + } +} +template<> const char *proto_enum_to_string(enums::ClimateAction value) { + switch (value) { + case enums::CLIMATE_ACTION_OFF: + return "CLIMATE_ACTION_OFF"; + case enums::CLIMATE_ACTION_COOLING: + return "CLIMATE_ACTION_COOLING"; + case enums::CLIMATE_ACTION_HEATING: + return "CLIMATE_ACTION_HEATING"; + case enums::CLIMATE_ACTION_IDLE: + return "CLIMATE_ACTION_IDLE"; + case enums::CLIMATE_ACTION_DRYING: + return "CLIMATE_ACTION_DRYING"; + case enums::CLIMATE_ACTION_FAN: + return "CLIMATE_ACTION_FAN"; + default: + return "UNKNOWN"; + } +} +template<> const char *proto_enum_to_string(enums::ClimatePreset value) { + switch (value) { + case enums::CLIMATE_PRESET_NONE: + return "CLIMATE_PRESET_NONE"; + case enums::CLIMATE_PRESET_HOME: + return "CLIMATE_PRESET_HOME"; + case enums::CLIMATE_PRESET_AWAY: + return "CLIMATE_PRESET_AWAY"; + case enums::CLIMATE_PRESET_BOOST: + return "CLIMATE_PRESET_BOOST"; + case enums::CLIMATE_PRESET_COMFORT: + return "CLIMATE_PRESET_COMFORT"; + case enums::CLIMATE_PRESET_ECO: + return "CLIMATE_PRESET_ECO"; + case enums::CLIMATE_PRESET_SLEEP: + return "CLIMATE_PRESET_SLEEP"; + case enums::CLIMATE_PRESET_ACTIVITY: + return "CLIMATE_PRESET_ACTIVITY"; + default: + return "UNKNOWN"; + } +} +#endif +#ifdef USE_NUMBER +template<> const char *proto_enum_to_string(enums::NumberMode value) { + switch (value) { + case enums::NUMBER_MODE_AUTO: + return "NUMBER_MODE_AUTO"; + case enums::NUMBER_MODE_BOX: + return "NUMBER_MODE_BOX"; + case enums::NUMBER_MODE_SLIDER: + return "NUMBER_MODE_SLIDER"; + default: + return "UNKNOWN"; + } +} +#endif +#ifdef USE_LOCK +template<> const char *proto_enum_to_string(enums::LockState value) { + switch (value) { + case enums::LOCK_STATE_NONE: + return "LOCK_STATE_NONE"; + case enums::LOCK_STATE_LOCKED: + return "LOCK_STATE_LOCKED"; + case enums::LOCK_STATE_UNLOCKED: + return "LOCK_STATE_UNLOCKED"; + case enums::LOCK_STATE_JAMMED: + return "LOCK_STATE_JAMMED"; + case enums::LOCK_STATE_LOCKING: + return "LOCK_STATE_LOCKING"; + case enums::LOCK_STATE_UNLOCKING: + return "LOCK_STATE_UNLOCKING"; + default: + return "UNKNOWN"; + } +} +template<> const char *proto_enum_to_string(enums::LockCommand value) { + switch (value) { + case enums::LOCK_UNLOCK: + return "LOCK_UNLOCK"; + case enums::LOCK_LOCK: + return "LOCK_LOCK"; + case enums::LOCK_OPEN: + return "LOCK_OPEN"; + default: + return "UNKNOWN"; + } +} +#endif +#ifdef USE_MEDIA_PLAYER +template<> const char *proto_enum_to_string(enums::MediaPlayerState value) { + switch (value) { + case enums::MEDIA_PLAYER_STATE_NONE: + return "MEDIA_PLAYER_STATE_NONE"; + case enums::MEDIA_PLAYER_STATE_IDLE: + return "MEDIA_PLAYER_STATE_IDLE"; + case enums::MEDIA_PLAYER_STATE_PLAYING: + return "MEDIA_PLAYER_STATE_PLAYING"; + case enums::MEDIA_PLAYER_STATE_PAUSED: + return "MEDIA_PLAYER_STATE_PAUSED"; + default: + return "UNKNOWN"; + } +} +template<> const char *proto_enum_to_string(enums::MediaPlayerCommand value) { + switch (value) { + case enums::MEDIA_PLAYER_COMMAND_PLAY: + return "MEDIA_PLAYER_COMMAND_PLAY"; + case enums::MEDIA_PLAYER_COMMAND_PAUSE: + return "MEDIA_PLAYER_COMMAND_PAUSE"; + case enums::MEDIA_PLAYER_COMMAND_STOP: + return "MEDIA_PLAYER_COMMAND_STOP"; + case enums::MEDIA_PLAYER_COMMAND_MUTE: + return "MEDIA_PLAYER_COMMAND_MUTE"; + case enums::MEDIA_PLAYER_COMMAND_UNMUTE: + return "MEDIA_PLAYER_COMMAND_UNMUTE"; + default: + return "UNKNOWN"; + } +} +template<> const char *proto_enum_to_string(enums::MediaPlayerFormatPurpose value) { + switch (value) { + case enums::MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT: + return "MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT"; + case enums::MEDIA_PLAYER_FORMAT_PURPOSE_ANNOUNCEMENT: + return "MEDIA_PLAYER_FORMAT_PURPOSE_ANNOUNCEMENT"; + default: + return "UNKNOWN"; + } +} +#endif +#ifdef USE_BLUETOOTH_PROXY +template<> +const char *proto_enum_to_string(enums::BluetoothDeviceRequestType value) { + switch (value) { + case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT: + return "BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT"; + case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT: + return "BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT"; + case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_PAIR: + return "BLUETOOTH_DEVICE_REQUEST_TYPE_PAIR"; + case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_UNPAIR: + return "BLUETOOTH_DEVICE_REQUEST_TYPE_UNPAIR"; + case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITH_CACHE: + return "BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITH_CACHE"; + case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE: + return "BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE"; + case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CLEAR_CACHE: + return "BLUETOOTH_DEVICE_REQUEST_TYPE_CLEAR_CACHE"; + default: + return "UNKNOWN"; + } +} +template<> const char *proto_enum_to_string(enums::BluetoothScannerState value) { + switch (value) { + case enums::BLUETOOTH_SCANNER_STATE_IDLE: + return "BLUETOOTH_SCANNER_STATE_IDLE"; + case enums::BLUETOOTH_SCANNER_STATE_STARTING: + return "BLUETOOTH_SCANNER_STATE_STARTING"; + case enums::BLUETOOTH_SCANNER_STATE_RUNNING: + return "BLUETOOTH_SCANNER_STATE_RUNNING"; + case enums::BLUETOOTH_SCANNER_STATE_FAILED: + return "BLUETOOTH_SCANNER_STATE_FAILED"; + case enums::BLUETOOTH_SCANNER_STATE_STOPPING: + return "BLUETOOTH_SCANNER_STATE_STOPPING"; + case enums::BLUETOOTH_SCANNER_STATE_STOPPED: + return "BLUETOOTH_SCANNER_STATE_STOPPED"; + default: + return "UNKNOWN"; + } +} +template<> const char *proto_enum_to_string(enums::BluetoothScannerMode value) { + switch (value) { + case enums::BLUETOOTH_SCANNER_MODE_PASSIVE: + return "BLUETOOTH_SCANNER_MODE_PASSIVE"; + case enums::BLUETOOTH_SCANNER_MODE_ACTIVE: + return "BLUETOOTH_SCANNER_MODE_ACTIVE"; + default: + return "UNKNOWN"; + } +} +#endif +template<> +const char *proto_enum_to_string(enums::VoiceAssistantSubscribeFlag value) { + switch (value) { + case enums::VOICE_ASSISTANT_SUBSCRIBE_NONE: + return "VOICE_ASSISTANT_SUBSCRIBE_NONE"; + case enums::VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO: + return "VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO"; + default: + return "UNKNOWN"; + } +} +template<> const char *proto_enum_to_string(enums::VoiceAssistantRequestFlag value) { + switch (value) { + case enums::VOICE_ASSISTANT_REQUEST_NONE: + return "VOICE_ASSISTANT_REQUEST_NONE"; + case enums::VOICE_ASSISTANT_REQUEST_USE_VAD: + return "VOICE_ASSISTANT_REQUEST_USE_VAD"; + case enums::VOICE_ASSISTANT_REQUEST_USE_WAKE_WORD: + return "VOICE_ASSISTANT_REQUEST_USE_WAKE_WORD"; + default: + return "UNKNOWN"; + } +} +#ifdef USE_VOICE_ASSISTANT +template<> const char *proto_enum_to_string(enums::VoiceAssistantEvent value) { + switch (value) { + case enums::VOICE_ASSISTANT_ERROR: + return "VOICE_ASSISTANT_ERROR"; + case enums::VOICE_ASSISTANT_RUN_START: + return "VOICE_ASSISTANT_RUN_START"; + case enums::VOICE_ASSISTANT_RUN_END: + return "VOICE_ASSISTANT_RUN_END"; + case enums::VOICE_ASSISTANT_STT_START: + return "VOICE_ASSISTANT_STT_START"; + case enums::VOICE_ASSISTANT_STT_END: + return "VOICE_ASSISTANT_STT_END"; + case enums::VOICE_ASSISTANT_INTENT_START: + return "VOICE_ASSISTANT_INTENT_START"; + case enums::VOICE_ASSISTANT_INTENT_END: + return "VOICE_ASSISTANT_INTENT_END"; + case enums::VOICE_ASSISTANT_TTS_START: + return "VOICE_ASSISTANT_TTS_START"; + case enums::VOICE_ASSISTANT_TTS_END: + return "VOICE_ASSISTANT_TTS_END"; + case enums::VOICE_ASSISTANT_WAKE_WORD_START: + return "VOICE_ASSISTANT_WAKE_WORD_START"; + case enums::VOICE_ASSISTANT_WAKE_WORD_END: + return "VOICE_ASSISTANT_WAKE_WORD_END"; + case enums::VOICE_ASSISTANT_STT_VAD_START: + return "VOICE_ASSISTANT_STT_VAD_START"; + case enums::VOICE_ASSISTANT_STT_VAD_END: + return "VOICE_ASSISTANT_STT_VAD_END"; + case enums::VOICE_ASSISTANT_TTS_STREAM_START: + return "VOICE_ASSISTANT_TTS_STREAM_START"; + case enums::VOICE_ASSISTANT_TTS_STREAM_END: + return "VOICE_ASSISTANT_TTS_STREAM_END"; + case enums::VOICE_ASSISTANT_INTENT_PROGRESS: + return "VOICE_ASSISTANT_INTENT_PROGRESS"; + default: + return "UNKNOWN"; + } +} +template<> const char *proto_enum_to_string(enums::VoiceAssistantTimerEvent value) { + switch (value) { + case enums::VOICE_ASSISTANT_TIMER_STARTED: + return "VOICE_ASSISTANT_TIMER_STARTED"; + case enums::VOICE_ASSISTANT_TIMER_UPDATED: + return "VOICE_ASSISTANT_TIMER_UPDATED"; + case enums::VOICE_ASSISTANT_TIMER_CANCELLED: + return "VOICE_ASSISTANT_TIMER_CANCELLED"; + case enums::VOICE_ASSISTANT_TIMER_FINISHED: + return "VOICE_ASSISTANT_TIMER_FINISHED"; + default: + return "UNKNOWN"; + } +} +#endif +#ifdef USE_ALARM_CONTROL_PANEL +template<> const char *proto_enum_to_string(enums::AlarmControlPanelState value) { + switch (value) { + case enums::ALARM_STATE_DISARMED: + return "ALARM_STATE_DISARMED"; + case enums::ALARM_STATE_ARMED_HOME: + return "ALARM_STATE_ARMED_HOME"; + case enums::ALARM_STATE_ARMED_AWAY: + return "ALARM_STATE_ARMED_AWAY"; + case enums::ALARM_STATE_ARMED_NIGHT: + return "ALARM_STATE_ARMED_NIGHT"; + case enums::ALARM_STATE_ARMED_VACATION: + return "ALARM_STATE_ARMED_VACATION"; + case enums::ALARM_STATE_ARMED_CUSTOM_BYPASS: + return "ALARM_STATE_ARMED_CUSTOM_BYPASS"; + case enums::ALARM_STATE_PENDING: + return "ALARM_STATE_PENDING"; + case enums::ALARM_STATE_ARMING: + return "ALARM_STATE_ARMING"; + case enums::ALARM_STATE_DISARMING: + return "ALARM_STATE_DISARMING"; + case enums::ALARM_STATE_TRIGGERED: + return "ALARM_STATE_TRIGGERED"; + default: + return "UNKNOWN"; + } +} +template<> +const char *proto_enum_to_string(enums::AlarmControlPanelStateCommand value) { + switch (value) { + case enums::ALARM_CONTROL_PANEL_DISARM: + return "ALARM_CONTROL_PANEL_DISARM"; + case enums::ALARM_CONTROL_PANEL_ARM_AWAY: + return "ALARM_CONTROL_PANEL_ARM_AWAY"; + case enums::ALARM_CONTROL_PANEL_ARM_HOME: + return "ALARM_CONTROL_PANEL_ARM_HOME"; + case enums::ALARM_CONTROL_PANEL_ARM_NIGHT: + return "ALARM_CONTROL_PANEL_ARM_NIGHT"; + case enums::ALARM_CONTROL_PANEL_ARM_VACATION: + return "ALARM_CONTROL_PANEL_ARM_VACATION"; + case enums::ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS: + return "ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS"; + case enums::ALARM_CONTROL_PANEL_TRIGGER: + return "ALARM_CONTROL_PANEL_TRIGGER"; + default: + return "UNKNOWN"; + } +} +#endif +#ifdef USE_TEXT +template<> const char *proto_enum_to_string(enums::TextMode value) { + switch (value) { + case enums::TEXT_MODE_TEXT: + return "TEXT_MODE_TEXT"; + case enums::TEXT_MODE_PASSWORD: + return "TEXT_MODE_PASSWORD"; + default: + return "UNKNOWN"; + } +} +#endif +#ifdef USE_VALVE +template<> const char *proto_enum_to_string(enums::ValveOperation value) { + switch (value) { + case enums::VALVE_OPERATION_IDLE: + return "VALVE_OPERATION_IDLE"; + case enums::VALVE_OPERATION_IS_OPENING: + return "VALVE_OPERATION_IS_OPENING"; + case enums::VALVE_OPERATION_IS_CLOSING: + return "VALVE_OPERATION_IS_CLOSING"; + default: + return "UNKNOWN"; + } +} +#endif +#ifdef USE_UPDATE +template<> const char *proto_enum_to_string(enums::UpdateCommand value) { + switch (value) { + case enums::UPDATE_COMMAND_NONE: + return "UPDATE_COMMAND_NONE"; + case enums::UPDATE_COMMAND_UPDATE: + return "UPDATE_COMMAND_UPDATE"; + case enums::UPDATE_COMMAND_CHECK: + return "UPDATE_COMMAND_CHECK"; + default: + return "UNKNOWN"; + } +} +#endif + +void HelloRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("HelloRequest {\n"); + out.append(" client_info: "); + out.append("'").append(this->client_info).append("'"); + out.append("\n"); + + out.append(" api_version_major: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->api_version_major); + out.append(buffer); + out.append("\n"); + + out.append(" api_version_minor: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->api_version_minor); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void HelloResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("HelloResponse {\n"); + out.append(" api_version_major: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->api_version_major); + out.append(buffer); + out.append("\n"); + + out.append(" api_version_minor: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->api_version_minor); + out.append(buffer); + out.append("\n"); + + out.append(" server_info: "); + out.append("'").append(this->server_info).append("'"); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + out.append("}"); +} +void ConnectRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ConnectRequest {\n"); + out.append(" password: "); + out.append("'").append(this->password).append("'"); + out.append("\n"); + out.append("}"); +} +void ConnectResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ConnectResponse {\n"); + out.append(" invalid_password: "); + out.append(YESNO(this->invalid_password)); + out.append("\n"); + out.append("}"); +} +void DisconnectRequest::dump_to(std::string &out) const { out.append("DisconnectRequest {}"); } +void DisconnectResponse::dump_to(std::string &out) const { out.append("DisconnectResponse {}"); } +void PingRequest::dump_to(std::string &out) const { out.append("PingRequest {}"); } +void PingResponse::dump_to(std::string &out) const { out.append("PingResponse {}"); } +void DeviceInfoRequest::dump_to(std::string &out) const { out.append("DeviceInfoRequest {}"); } +void AreaInfo::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("AreaInfo {\n"); + out.append(" area_id: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->area_id); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + out.append("}"); +} +void DeviceInfo::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("DeviceInfo {\n"); + out.append(" device_id: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" area_id: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->area_id); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void DeviceInfoResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("DeviceInfoResponse {\n"); + out.append(" uses_password: "); + out.append(YESNO(this->uses_password)); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" mac_address: "); + out.append("'").append(this->mac_address).append("'"); + out.append("\n"); + + out.append(" esphome_version: "); + out.append("'").append(this->esphome_version).append("'"); + out.append("\n"); + + out.append(" compilation_time: "); + out.append("'").append(this->compilation_time).append("'"); + out.append("\n"); + + out.append(" model: "); + out.append("'").append(this->model).append("'"); + out.append("\n"); + + out.append(" has_deep_sleep: "); + out.append(YESNO(this->has_deep_sleep)); + out.append("\n"); + + out.append(" project_name: "); + out.append("'").append(this->project_name).append("'"); + out.append("\n"); + + out.append(" project_version: "); + out.append("'").append(this->project_version).append("'"); + out.append("\n"); + + out.append(" webserver_port: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->webserver_port); + out.append(buffer); + out.append("\n"); + + out.append(" legacy_bluetooth_proxy_version: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->legacy_bluetooth_proxy_version); + out.append(buffer); + out.append("\n"); + + out.append(" bluetooth_proxy_feature_flags: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->bluetooth_proxy_feature_flags); + out.append(buffer); + out.append("\n"); + + out.append(" manufacturer: "); + out.append("'").append(this->manufacturer).append("'"); + out.append("\n"); + + out.append(" friendly_name: "); + out.append("'").append(this->friendly_name).append("'"); + out.append("\n"); + + out.append(" legacy_voice_assistant_version: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->legacy_voice_assistant_version); + out.append(buffer); + out.append("\n"); + + out.append(" voice_assistant_feature_flags: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->voice_assistant_feature_flags); + out.append(buffer); + out.append("\n"); + + out.append(" suggested_area: "); + out.append("'").append(this->suggested_area).append("'"); + out.append("\n"); + + out.append(" bluetooth_mac_address: "); + out.append("'").append(this->bluetooth_mac_address).append("'"); + out.append("\n"); + + out.append(" api_encryption_supported: "); + out.append(YESNO(this->api_encryption_supported)); + out.append("\n"); + + for (const auto &it : this->devices) { + out.append(" devices: "); + it.dump_to(out); + out.append("\n"); + } + + for (const auto &it : this->areas) { + out.append(" areas: "); + it.dump_to(out); + out.append("\n"); + } + + out.append(" area: "); + this->area.dump_to(out); + out.append("\n"); + out.append("}"); +} +void ListEntitiesRequest::dump_to(std::string &out) const { out.append("ListEntitiesRequest {}"); } +void ListEntitiesDoneResponse::dump_to(std::string &out) const { out.append("ListEntitiesDoneResponse {}"); } +void SubscribeStatesRequest::dump_to(std::string &out) const { out.append("SubscribeStatesRequest {}"); } +#ifdef USE_BINARY_SENSOR +void ListEntitiesBinarySensorResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ListEntitiesBinarySensorResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" unique_id: "); + out.append("'").append(this->unique_id).append("'"); + out.append("\n"); + + out.append(" device_class: "); + out.append("'").append(this->device_class).append("'"); + out.append("\n"); + + out.append(" is_status_binary_sensor: "); + out.append(YESNO(this->is_status_binary_sensor)); + out.append("\n"); + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); + + out.append(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); + + out.append(" device_id: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void BinarySensorStateResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BinarySensorStateResponse {\n"); + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" state: "); + out.append(YESNO(this->state)); + out.append("\n"); + + out.append(" missing_state: "); + out.append(YESNO(this->missing_state)); + out.append("\n"); + + out.append(" device_id: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +#endif +#ifdef USE_COVER +void ListEntitiesCoverResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ListEntitiesCoverResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" unique_id: "); + out.append("'").append(this->unique_id).append("'"); + out.append("\n"); + + out.append(" assumed_state: "); + out.append(YESNO(this->assumed_state)); + out.append("\n"); + + out.append(" supports_position: "); + out.append(YESNO(this->supports_position)); + out.append("\n"); + + out.append(" supports_tilt: "); + out.append(YESNO(this->supports_tilt)); + out.append("\n"); + + out.append(" device_class: "); + out.append("'").append(this->device_class).append("'"); + out.append("\n"); + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); + + out.append(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); + + out.append(" supports_stop: "); + out.append(YESNO(this->supports_stop)); + out.append("\n"); + + out.append(" device_id: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void CoverStateResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("CoverStateResponse {\n"); + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" legacy_state: "); + out.append(proto_enum_to_string(this->legacy_state)); + out.append("\n"); + + out.append(" position: "); + snprintf(buffer, sizeof(buffer), "%g", this->position); + out.append(buffer); + out.append("\n"); + + out.append(" tilt: "); + snprintf(buffer, sizeof(buffer), "%g", this->tilt); + out.append(buffer); + out.append("\n"); + + out.append(" current_operation: "); + out.append(proto_enum_to_string(this->current_operation)); + out.append("\n"); + + out.append(" device_id: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void CoverCommandRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("CoverCommandRequest {\n"); + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" has_legacy_command: "); + out.append(YESNO(this->has_legacy_command)); + out.append("\n"); + + out.append(" legacy_command: "); + out.append(proto_enum_to_string(this->legacy_command)); + out.append("\n"); + + out.append(" has_position: "); + out.append(YESNO(this->has_position)); + out.append("\n"); + + out.append(" position: "); + snprintf(buffer, sizeof(buffer), "%g", this->position); + out.append(buffer); + out.append("\n"); + + out.append(" has_tilt: "); + out.append(YESNO(this->has_tilt)); + out.append("\n"); + + out.append(" tilt: "); + snprintf(buffer, sizeof(buffer), "%g", this->tilt); + out.append(buffer); + out.append("\n"); + + out.append(" stop: "); + out.append(YESNO(this->stop)); + out.append("\n"); + out.append("}"); +} +#endif +#ifdef USE_FAN +void ListEntitiesFanResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ListEntitiesFanResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" unique_id: "); + out.append("'").append(this->unique_id).append("'"); + out.append("\n"); + + out.append(" supports_oscillation: "); + out.append(YESNO(this->supports_oscillation)); + out.append("\n"); + + out.append(" supports_speed: "); + out.append(YESNO(this->supports_speed)); + out.append("\n"); + + out.append(" supports_direction: "); + out.append(YESNO(this->supports_direction)); + out.append("\n"); + + out.append(" supported_speed_count: "); + snprintf(buffer, sizeof(buffer), "%" PRId32, this->supported_speed_count); + out.append(buffer); + out.append("\n"); + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); + + out.append(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); + + for (const auto &it : this->supported_preset_modes) { + out.append(" supported_preset_modes: "); + out.append("'").append(it).append("'"); + out.append("\n"); + } + + out.append(" device_id: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void FanStateResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("FanStateResponse {\n"); + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" state: "); + out.append(YESNO(this->state)); + out.append("\n"); + + out.append(" oscillating: "); + out.append(YESNO(this->oscillating)); + out.append("\n"); + + out.append(" speed: "); + out.append(proto_enum_to_string(this->speed)); + out.append("\n"); + + out.append(" direction: "); + out.append(proto_enum_to_string(this->direction)); + out.append("\n"); + + out.append(" speed_level: "); + snprintf(buffer, sizeof(buffer), "%" PRId32, this->speed_level); + out.append(buffer); + out.append("\n"); + + out.append(" preset_mode: "); + out.append("'").append(this->preset_mode).append("'"); + out.append("\n"); + + out.append(" device_id: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void FanCommandRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("FanCommandRequest {\n"); + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" has_state: "); + out.append(YESNO(this->has_state)); + out.append("\n"); + + out.append(" state: "); + out.append(YESNO(this->state)); + out.append("\n"); + + out.append(" has_speed: "); + out.append(YESNO(this->has_speed)); + out.append("\n"); + + out.append(" speed: "); + out.append(proto_enum_to_string(this->speed)); + out.append("\n"); + + out.append(" has_oscillating: "); + out.append(YESNO(this->has_oscillating)); + out.append("\n"); + + out.append(" oscillating: "); + out.append(YESNO(this->oscillating)); + out.append("\n"); + + out.append(" has_direction: "); + out.append(YESNO(this->has_direction)); + out.append("\n"); + + out.append(" direction: "); + out.append(proto_enum_to_string(this->direction)); + out.append("\n"); + + out.append(" has_speed_level: "); + out.append(YESNO(this->has_speed_level)); + out.append("\n"); + + out.append(" speed_level: "); + snprintf(buffer, sizeof(buffer), "%" PRId32, this->speed_level); + out.append(buffer); + out.append("\n"); + + out.append(" has_preset_mode: "); + out.append(YESNO(this->has_preset_mode)); + out.append("\n"); + + out.append(" preset_mode: "); + out.append("'").append(this->preset_mode).append("'"); + out.append("\n"); + out.append("}"); +} +#endif +#ifdef USE_LIGHT +void ListEntitiesLightResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ListEntitiesLightResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" unique_id: "); + out.append("'").append(this->unique_id).append("'"); + out.append("\n"); + + for (const auto &it : this->supported_color_modes) { + out.append(" supported_color_modes: "); + out.append(proto_enum_to_string(it)); + out.append("\n"); + } + + out.append(" legacy_supports_brightness: "); + out.append(YESNO(this->legacy_supports_brightness)); + out.append("\n"); + + out.append(" legacy_supports_rgb: "); + out.append(YESNO(this->legacy_supports_rgb)); + out.append("\n"); + + out.append(" legacy_supports_white_value: "); + out.append(YESNO(this->legacy_supports_white_value)); + out.append("\n"); + + out.append(" legacy_supports_color_temperature: "); + out.append(YESNO(this->legacy_supports_color_temperature)); + out.append("\n"); + + out.append(" min_mireds: "); + snprintf(buffer, sizeof(buffer), "%g", this->min_mireds); + out.append(buffer); + out.append("\n"); + + out.append(" max_mireds: "); + snprintf(buffer, sizeof(buffer), "%g", this->max_mireds); + out.append(buffer); + out.append("\n"); + + for (const auto &it : this->effects) { + out.append(" effects: "); + out.append("'").append(it).append("'"); + out.append("\n"); + } + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); + + out.append(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); + + out.append(" device_id: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void LightStateResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("LightStateResponse {\n"); + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" state: "); + out.append(YESNO(this->state)); + out.append("\n"); + + out.append(" brightness: "); + snprintf(buffer, sizeof(buffer), "%g", this->brightness); + out.append(buffer); + out.append("\n"); + + out.append(" color_mode: "); + out.append(proto_enum_to_string(this->color_mode)); + out.append("\n"); + + out.append(" color_brightness: "); + snprintf(buffer, sizeof(buffer), "%g", this->color_brightness); + out.append(buffer); + out.append("\n"); + + out.append(" red: "); + snprintf(buffer, sizeof(buffer), "%g", this->red); + out.append(buffer); + out.append("\n"); + + out.append(" green: "); + snprintf(buffer, sizeof(buffer), "%g", this->green); + out.append(buffer); + out.append("\n"); + + out.append(" blue: "); + snprintf(buffer, sizeof(buffer), "%g", this->blue); + out.append(buffer); + out.append("\n"); + + out.append(" white: "); + snprintf(buffer, sizeof(buffer), "%g", this->white); + out.append(buffer); + out.append("\n"); + + out.append(" color_temperature: "); + snprintf(buffer, sizeof(buffer), "%g", this->color_temperature); + out.append(buffer); + out.append("\n"); + + out.append(" cold_white: "); + snprintf(buffer, sizeof(buffer), "%g", this->cold_white); + out.append(buffer); + out.append("\n"); + + out.append(" warm_white: "); + snprintf(buffer, sizeof(buffer), "%g", this->warm_white); + out.append(buffer); + out.append("\n"); + + out.append(" effect: "); + out.append("'").append(this->effect).append("'"); + out.append("\n"); + + out.append(" device_id: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void LightCommandRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("LightCommandRequest {\n"); + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" has_state: "); + out.append(YESNO(this->has_state)); + out.append("\n"); + + out.append(" state: "); + out.append(YESNO(this->state)); + out.append("\n"); + + out.append(" has_brightness: "); + out.append(YESNO(this->has_brightness)); + out.append("\n"); + + out.append(" brightness: "); + snprintf(buffer, sizeof(buffer), "%g", this->brightness); + out.append(buffer); + out.append("\n"); + + out.append(" has_color_mode: "); + out.append(YESNO(this->has_color_mode)); + out.append("\n"); + + out.append(" color_mode: "); + out.append(proto_enum_to_string(this->color_mode)); + out.append("\n"); + + out.append(" has_color_brightness: "); + out.append(YESNO(this->has_color_brightness)); + out.append("\n"); + + out.append(" color_brightness: "); + snprintf(buffer, sizeof(buffer), "%g", this->color_brightness); + out.append(buffer); + out.append("\n"); + + out.append(" has_rgb: "); + out.append(YESNO(this->has_rgb)); + out.append("\n"); + + out.append(" red: "); + snprintf(buffer, sizeof(buffer), "%g", this->red); + out.append(buffer); + out.append("\n"); + + out.append(" green: "); + snprintf(buffer, sizeof(buffer), "%g", this->green); + out.append(buffer); + out.append("\n"); + + out.append(" blue: "); + snprintf(buffer, sizeof(buffer), "%g", this->blue); + out.append(buffer); + out.append("\n"); + + out.append(" has_white: "); + out.append(YESNO(this->has_white)); + out.append("\n"); + + out.append(" white: "); + snprintf(buffer, sizeof(buffer), "%g", this->white); + out.append(buffer); + out.append("\n"); + + out.append(" has_color_temperature: "); + out.append(YESNO(this->has_color_temperature)); + out.append("\n"); + + out.append(" color_temperature: "); + snprintf(buffer, sizeof(buffer), "%g", this->color_temperature); + out.append(buffer); + out.append("\n"); + + out.append(" has_cold_white: "); + out.append(YESNO(this->has_cold_white)); + out.append("\n"); + + out.append(" cold_white: "); + snprintf(buffer, sizeof(buffer), "%g", this->cold_white); + out.append(buffer); + out.append("\n"); + + out.append(" has_warm_white: "); + out.append(YESNO(this->has_warm_white)); + out.append("\n"); + + out.append(" warm_white: "); + snprintf(buffer, sizeof(buffer), "%g", this->warm_white); + out.append(buffer); + out.append("\n"); + + out.append(" has_transition_length: "); + out.append(YESNO(this->has_transition_length)); + out.append("\n"); + + out.append(" transition_length: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->transition_length); + out.append(buffer); + out.append("\n"); + + out.append(" has_flash_length: "); + out.append(YESNO(this->has_flash_length)); + out.append("\n"); + + out.append(" flash_length: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->flash_length); + out.append(buffer); + out.append("\n"); + + out.append(" has_effect: "); + out.append(YESNO(this->has_effect)); + out.append("\n"); + + out.append(" effect: "); + out.append("'").append(this->effect).append("'"); + out.append("\n"); + out.append("}"); +} +#endif +#ifdef USE_SENSOR +void ListEntitiesSensorResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ListEntitiesSensorResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" unique_id: "); + out.append("'").append(this->unique_id).append("'"); + out.append("\n"); + + out.append(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); + + out.append(" unit_of_measurement: "); + out.append("'").append(this->unit_of_measurement).append("'"); + out.append("\n"); + + out.append(" accuracy_decimals: "); + snprintf(buffer, sizeof(buffer), "%" PRId32, this->accuracy_decimals); + out.append(buffer); + out.append("\n"); + + out.append(" force_update: "); + out.append(YESNO(this->force_update)); + out.append("\n"); + + out.append(" device_class: "); + out.append("'").append(this->device_class).append("'"); + out.append("\n"); + + out.append(" state_class: "); + out.append(proto_enum_to_string(this->state_class)); + out.append("\n"); + + out.append(" legacy_last_reset_type: "); + out.append(proto_enum_to_string(this->legacy_last_reset_type)); + out.append("\n"); + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); + + out.append(" device_id: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void SensorStateResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("SensorStateResponse {\n"); + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" state: "); + snprintf(buffer, sizeof(buffer), "%g", this->state); + out.append(buffer); + out.append("\n"); + + out.append(" missing_state: "); + out.append(YESNO(this->missing_state)); + out.append("\n"); + + out.append(" device_id: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +#endif +#ifdef USE_SWITCH +void ListEntitiesSwitchResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ListEntitiesSwitchResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" unique_id: "); + out.append("'").append(this->unique_id).append("'"); + out.append("\n"); + + out.append(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); + + out.append(" assumed_state: "); + out.append(YESNO(this->assumed_state)); + out.append("\n"); + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); + + out.append(" device_class: "); + out.append("'").append(this->device_class).append("'"); + out.append("\n"); + + out.append(" device_id: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void SwitchStateResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("SwitchStateResponse {\n"); + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" state: "); + out.append(YESNO(this->state)); + out.append("\n"); + + out.append(" device_id: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void SwitchCommandRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("SwitchCommandRequest {\n"); + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" state: "); + out.append(YESNO(this->state)); + out.append("\n"); + out.append("}"); +} +#endif +#ifdef USE_TEXT_SENSOR +void ListEntitiesTextSensorResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ListEntitiesTextSensorResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" unique_id: "); + out.append("'").append(this->unique_id).append("'"); + out.append("\n"); + + out.append(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); + + out.append(" device_class: "); + out.append("'").append(this->device_class).append("'"); + out.append("\n"); + + out.append(" device_id: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void TextSensorStateResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("TextSensorStateResponse {\n"); + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" state: "); + out.append("'").append(this->state).append("'"); + out.append("\n"); + + out.append(" missing_state: "); + out.append(YESNO(this->missing_state)); + out.append("\n"); + + out.append(" device_id: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +#endif +void SubscribeLogsRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("SubscribeLogsRequest {\n"); + out.append(" level: "); + out.append(proto_enum_to_string(this->level)); + out.append("\n"); + + out.append(" dump_config: "); + out.append(YESNO(this->dump_config)); + out.append("\n"); + out.append("}"); +} +void SubscribeLogsResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("SubscribeLogsResponse {\n"); + out.append(" level: "); + out.append(proto_enum_to_string(this->level)); + out.append("\n"); + + out.append(" message: "); + out.append(format_hex_pretty(this->message)); + out.append("\n"); + + out.append(" send_failed: "); + out.append(YESNO(this->send_failed)); + out.append("\n"); + out.append("}"); +} +#ifdef USE_API_NOISE +void NoiseEncryptionSetKeyRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("NoiseEncryptionSetKeyRequest {\n"); + out.append(" key: "); + out.append(format_hex_pretty(this->key)); + out.append("\n"); + out.append("}"); +} +void NoiseEncryptionSetKeyResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("NoiseEncryptionSetKeyResponse {\n"); + out.append(" success: "); + out.append(YESNO(this->success)); + out.append("\n"); + out.append("}"); +} +#endif +void SubscribeHomeassistantServicesRequest::dump_to(std::string &out) const { + out.append("SubscribeHomeassistantServicesRequest {}"); +} +void HomeassistantServiceMap::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("HomeassistantServiceMap {\n"); + out.append(" key: "); + out.append("'").append(this->key).append("'"); + out.append("\n"); + + out.append(" value: "); + out.append("'").append(this->value).append("'"); + out.append("\n"); + out.append("}"); +} +void HomeassistantServiceResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("HomeassistantServiceResponse {\n"); + out.append(" service: "); + out.append("'").append(this->service).append("'"); + out.append("\n"); + + for (const auto &it : this->data) { + out.append(" data: "); + it.dump_to(out); + out.append("\n"); + } + + for (const auto &it : this->data_template) { + out.append(" data_template: "); + it.dump_to(out); + out.append("\n"); + } + + for (const auto &it : this->variables) { + out.append(" variables: "); + it.dump_to(out); + out.append("\n"); + } + + out.append(" is_event: "); + out.append(YESNO(this->is_event)); + out.append("\n"); + out.append("}"); +} +void SubscribeHomeAssistantStatesRequest::dump_to(std::string &out) const { + out.append("SubscribeHomeAssistantStatesRequest {}"); +} +void SubscribeHomeAssistantStateResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("SubscribeHomeAssistantStateResponse {\n"); + out.append(" entity_id: "); + out.append("'").append(this->entity_id).append("'"); + out.append("\n"); + + out.append(" attribute: "); + out.append("'").append(this->attribute).append("'"); + out.append("\n"); + + out.append(" once: "); + out.append(YESNO(this->once)); + out.append("\n"); + out.append("}"); +} +void HomeAssistantStateResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("HomeAssistantStateResponse {\n"); + out.append(" entity_id: "); + out.append("'").append(this->entity_id).append("'"); + out.append("\n"); + + out.append(" state: "); + out.append("'").append(this->state).append("'"); + out.append("\n"); + + out.append(" attribute: "); + out.append("'").append(this->attribute).append("'"); + out.append("\n"); + out.append("}"); +} +void GetTimeRequest::dump_to(std::string &out) const { out.append("GetTimeRequest {}"); } +void GetTimeResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("GetTimeResponse {\n"); + out.append(" epoch_seconds: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->epoch_seconds); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void ListEntitiesServicesArgument::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ListEntitiesServicesArgument {\n"); + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" type: "); + out.append(proto_enum_to_string(this->type)); + out.append("\n"); + out.append("}"); +} +void ListEntitiesServicesResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ListEntitiesServicesResponse {\n"); + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + for (const auto &it : this->args) { + out.append(" args: "); + it.dump_to(out); + out.append("\n"); + } + out.append("}"); +} +void ExecuteServiceArgument::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ExecuteServiceArgument {\n"); + out.append(" bool_: "); + out.append(YESNO(this->bool_)); + out.append("\n"); + + out.append(" legacy_int: "); + snprintf(buffer, sizeof(buffer), "%" PRId32, this->legacy_int); + out.append(buffer); + out.append("\n"); + + out.append(" float_: "); + snprintf(buffer, sizeof(buffer), "%g", this->float_); + out.append(buffer); + out.append("\n"); + + out.append(" string_: "); + out.append("'").append(this->string_).append("'"); + out.append("\n"); + + out.append(" int_: "); + snprintf(buffer, sizeof(buffer), "%" PRId32, this->int_); + out.append(buffer); + out.append("\n"); + + for (const auto it : this->bool_array) { + out.append(" bool_array: "); + out.append(YESNO(it)); + out.append("\n"); + } + + for (const auto &it : this->int_array) { + out.append(" int_array: "); + snprintf(buffer, sizeof(buffer), "%" PRId32, it); + out.append(buffer); + out.append("\n"); + } + + for (const auto &it : this->float_array) { + out.append(" float_array: "); + snprintf(buffer, sizeof(buffer), "%g", it); + out.append(buffer); + out.append("\n"); + } + + for (const auto &it : this->string_array) { + out.append(" string_array: "); + out.append("'").append(it).append("'"); + out.append("\n"); + } + out.append("}"); +} +void ExecuteServiceRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ExecuteServiceRequest {\n"); + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + for (const auto &it : this->args) { + out.append(" args: "); + it.dump_to(out); + out.append("\n"); + } + out.append("}"); +} +#ifdef USE_CAMERA +void ListEntitiesCameraResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ListEntitiesCameraResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" unique_id: "); + out.append("'").append(this->unique_id).append("'"); + out.append("\n"); + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); + + out.append(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); + + out.append(" device_id: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void CameraImageResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("CameraImageResponse {\n"); + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" data: "); + out.append(format_hex_pretty(this->data)); + out.append("\n"); + + out.append(" done: "); + out.append(YESNO(this->done)); + out.append("\n"); + out.append("}"); +} +void CameraImageRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("CameraImageRequest {\n"); + out.append(" single: "); + out.append(YESNO(this->single)); + out.append("\n"); + + out.append(" stream: "); + out.append(YESNO(this->stream)); + out.append("\n"); + out.append("}"); +} +#endif +#ifdef USE_CLIMATE +void ListEntitiesClimateResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ListEntitiesClimateResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" unique_id: "); + out.append("'").append(this->unique_id).append("'"); + out.append("\n"); + + out.append(" supports_current_temperature: "); + out.append(YESNO(this->supports_current_temperature)); + out.append("\n"); + + out.append(" supports_two_point_target_temperature: "); + out.append(YESNO(this->supports_two_point_target_temperature)); + out.append("\n"); + + for (const auto &it : this->supported_modes) { + out.append(" supported_modes: "); + out.append(proto_enum_to_string(it)); + out.append("\n"); + } + + out.append(" visual_min_temperature: "); + snprintf(buffer, sizeof(buffer), "%g", this->visual_min_temperature); + out.append(buffer); + out.append("\n"); + + out.append(" visual_max_temperature: "); + snprintf(buffer, sizeof(buffer), "%g", this->visual_max_temperature); + out.append(buffer); + out.append("\n"); + + out.append(" visual_target_temperature_step: "); + snprintf(buffer, sizeof(buffer), "%g", this->visual_target_temperature_step); + out.append(buffer); + out.append("\n"); + + out.append(" legacy_supports_away: "); + out.append(YESNO(this->legacy_supports_away)); + out.append("\n"); + + out.append(" supports_action: "); + out.append(YESNO(this->supports_action)); + out.append("\n"); + + for (const auto &it : this->supported_fan_modes) { + out.append(" supported_fan_modes: "); + out.append(proto_enum_to_string(it)); + out.append("\n"); + } + + for (const auto &it : this->supported_swing_modes) { + out.append(" supported_swing_modes: "); + out.append(proto_enum_to_string(it)); + out.append("\n"); + } + + for (const auto &it : this->supported_custom_fan_modes) { + out.append(" supported_custom_fan_modes: "); + out.append("'").append(it).append("'"); + out.append("\n"); + } + + for (const auto &it : this->supported_presets) { + out.append(" supported_presets: "); + out.append(proto_enum_to_string(it)); + out.append("\n"); + } + + for (const auto &it : this->supported_custom_presets) { + out.append(" supported_custom_presets: "); + out.append("'").append(it).append("'"); + out.append("\n"); + } + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); + + out.append(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); + + out.append(" visual_current_temperature_step: "); + snprintf(buffer, sizeof(buffer), "%g", this->visual_current_temperature_step); + out.append(buffer); + out.append("\n"); + + out.append(" supports_current_humidity: "); + out.append(YESNO(this->supports_current_humidity)); + out.append("\n"); + + out.append(" supports_target_humidity: "); + out.append(YESNO(this->supports_target_humidity)); + out.append("\n"); + + out.append(" visual_min_humidity: "); + snprintf(buffer, sizeof(buffer), "%g", this->visual_min_humidity); + out.append(buffer); + out.append("\n"); + + out.append(" visual_max_humidity: "); + snprintf(buffer, sizeof(buffer), "%g", this->visual_max_humidity); + out.append(buffer); + out.append("\n"); + + out.append(" device_id: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void ClimateStateResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ClimateStateResponse {\n"); + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" mode: "); + out.append(proto_enum_to_string(this->mode)); + out.append("\n"); + + out.append(" current_temperature: "); + snprintf(buffer, sizeof(buffer), "%g", this->current_temperature); + out.append(buffer); + out.append("\n"); + + out.append(" target_temperature: "); + snprintf(buffer, sizeof(buffer), "%g", this->target_temperature); + out.append(buffer); + out.append("\n"); + + out.append(" target_temperature_low: "); + snprintf(buffer, sizeof(buffer), "%g", this->target_temperature_low); + out.append(buffer); + out.append("\n"); + + out.append(" target_temperature_high: "); + snprintf(buffer, sizeof(buffer), "%g", this->target_temperature_high); + out.append(buffer); + out.append("\n"); + + out.append(" unused_legacy_away: "); + out.append(YESNO(this->unused_legacy_away)); + out.append("\n"); + + out.append(" action: "); + out.append(proto_enum_to_string(this->action)); + out.append("\n"); + + out.append(" fan_mode: "); + out.append(proto_enum_to_string(this->fan_mode)); + out.append("\n"); + + out.append(" swing_mode: "); + out.append(proto_enum_to_string(this->swing_mode)); + out.append("\n"); + + out.append(" custom_fan_mode: "); + out.append("'").append(this->custom_fan_mode).append("'"); + out.append("\n"); + + out.append(" preset: "); + out.append(proto_enum_to_string(this->preset)); + out.append("\n"); + + out.append(" custom_preset: "); + out.append("'").append(this->custom_preset).append("'"); + out.append("\n"); + + out.append(" current_humidity: "); + snprintf(buffer, sizeof(buffer), "%g", this->current_humidity); + out.append(buffer); + out.append("\n"); + + out.append(" target_humidity: "); + snprintf(buffer, sizeof(buffer), "%g", this->target_humidity); + out.append(buffer); + out.append("\n"); + + out.append(" device_id: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void ClimateCommandRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ClimateCommandRequest {\n"); + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" has_mode: "); + out.append(YESNO(this->has_mode)); + out.append("\n"); + + out.append(" mode: "); + out.append(proto_enum_to_string(this->mode)); + out.append("\n"); + + out.append(" has_target_temperature: "); + out.append(YESNO(this->has_target_temperature)); + out.append("\n"); + + out.append(" target_temperature: "); + snprintf(buffer, sizeof(buffer), "%g", this->target_temperature); + out.append(buffer); + out.append("\n"); + + out.append(" has_target_temperature_low: "); + out.append(YESNO(this->has_target_temperature_low)); + out.append("\n"); + + out.append(" target_temperature_low: "); + snprintf(buffer, sizeof(buffer), "%g", this->target_temperature_low); + out.append(buffer); + out.append("\n"); + + out.append(" has_target_temperature_high: "); + out.append(YESNO(this->has_target_temperature_high)); + out.append("\n"); + + out.append(" target_temperature_high: "); + snprintf(buffer, sizeof(buffer), "%g", this->target_temperature_high); + out.append(buffer); + out.append("\n"); + + out.append(" unused_has_legacy_away: "); + out.append(YESNO(this->unused_has_legacy_away)); + out.append("\n"); + + out.append(" unused_legacy_away: "); + out.append(YESNO(this->unused_legacy_away)); + out.append("\n"); + + out.append(" has_fan_mode: "); + out.append(YESNO(this->has_fan_mode)); + out.append("\n"); + + out.append(" fan_mode: "); + out.append(proto_enum_to_string(this->fan_mode)); + out.append("\n"); + + out.append(" has_swing_mode: "); + out.append(YESNO(this->has_swing_mode)); + out.append("\n"); + + out.append(" swing_mode: "); + out.append(proto_enum_to_string(this->swing_mode)); + out.append("\n"); + + out.append(" has_custom_fan_mode: "); + out.append(YESNO(this->has_custom_fan_mode)); + out.append("\n"); + + out.append(" custom_fan_mode: "); + out.append("'").append(this->custom_fan_mode).append("'"); + out.append("\n"); + + out.append(" has_preset: "); + out.append(YESNO(this->has_preset)); + out.append("\n"); + + out.append(" preset: "); + out.append(proto_enum_to_string(this->preset)); + out.append("\n"); + + out.append(" has_custom_preset: "); + out.append(YESNO(this->has_custom_preset)); + out.append("\n"); + + out.append(" custom_preset: "); + out.append("'").append(this->custom_preset).append("'"); + out.append("\n"); + + out.append(" has_target_humidity: "); + out.append(YESNO(this->has_target_humidity)); + out.append("\n"); + + out.append(" target_humidity: "); + snprintf(buffer, sizeof(buffer), "%g", this->target_humidity); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +#endif +#ifdef USE_NUMBER +void ListEntitiesNumberResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ListEntitiesNumberResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" unique_id: "); + out.append("'").append(this->unique_id).append("'"); + out.append("\n"); + + out.append(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); + + out.append(" min_value: "); + snprintf(buffer, sizeof(buffer), "%g", this->min_value); + out.append(buffer); + out.append("\n"); + + out.append(" max_value: "); + snprintf(buffer, sizeof(buffer), "%g", this->max_value); + out.append(buffer); + out.append("\n"); + + out.append(" step: "); + snprintf(buffer, sizeof(buffer), "%g", this->step); + out.append(buffer); + out.append("\n"); + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); + + out.append(" unit_of_measurement: "); + out.append("'").append(this->unit_of_measurement).append("'"); + out.append("\n"); + + out.append(" mode: "); + out.append(proto_enum_to_string(this->mode)); + out.append("\n"); + + out.append(" device_class: "); + out.append("'").append(this->device_class).append("'"); + out.append("\n"); + + out.append(" device_id: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void NumberStateResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("NumberStateResponse {\n"); + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" state: "); + snprintf(buffer, sizeof(buffer), "%g", this->state); + out.append(buffer); + out.append("\n"); + + out.append(" missing_state: "); + out.append(YESNO(this->missing_state)); + out.append("\n"); + + out.append(" device_id: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void NumberCommandRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("NumberCommandRequest {\n"); + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" state: "); + snprintf(buffer, sizeof(buffer), "%g", this->state); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +#endif +#ifdef USE_SELECT +void ListEntitiesSelectResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ListEntitiesSelectResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" unique_id: "); + out.append("'").append(this->unique_id).append("'"); + out.append("\n"); + + out.append(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); + + for (const auto &it : this->options) { + out.append(" options: "); + out.append("'").append(it).append("'"); + out.append("\n"); + } + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); + + out.append(" device_id: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void SelectStateResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("SelectStateResponse {\n"); + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" state: "); + out.append("'").append(this->state).append("'"); + out.append("\n"); + + out.append(" missing_state: "); + out.append(YESNO(this->missing_state)); + out.append("\n"); + + out.append(" device_id: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void SelectCommandRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("SelectCommandRequest {\n"); + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" state: "); + out.append("'").append(this->state).append("'"); + out.append("\n"); + out.append("}"); +} +#endif +#ifdef USE_SIREN +void ListEntitiesSirenResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ListEntitiesSirenResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" unique_id: "); + out.append("'").append(this->unique_id).append("'"); + out.append("\n"); + + out.append(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); + + for (const auto &it : this->tones) { + out.append(" tones: "); + out.append("'").append(it).append("'"); + out.append("\n"); + } + + out.append(" supports_duration: "); + out.append(YESNO(this->supports_duration)); + out.append("\n"); + + out.append(" supports_volume: "); + out.append(YESNO(this->supports_volume)); + out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); + + out.append(" device_id: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void SirenStateResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("SirenStateResponse {\n"); + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" state: "); + out.append(YESNO(this->state)); + out.append("\n"); + + out.append(" device_id: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void SirenCommandRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("SirenCommandRequest {\n"); + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" has_state: "); + out.append(YESNO(this->has_state)); + out.append("\n"); + + out.append(" state: "); + out.append(YESNO(this->state)); + out.append("\n"); + + out.append(" has_tone: "); + out.append(YESNO(this->has_tone)); + out.append("\n"); + + out.append(" tone: "); + out.append("'").append(this->tone).append("'"); + out.append("\n"); + + out.append(" has_duration: "); + out.append(YESNO(this->has_duration)); + out.append("\n"); + + out.append(" duration: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->duration); + out.append(buffer); + out.append("\n"); + + out.append(" has_volume: "); + out.append(YESNO(this->has_volume)); + out.append("\n"); + + out.append(" volume: "); + snprintf(buffer, sizeof(buffer), "%g", this->volume); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +#endif +#ifdef USE_LOCK +void ListEntitiesLockResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ListEntitiesLockResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" unique_id: "); + out.append("'").append(this->unique_id).append("'"); + out.append("\n"); + + out.append(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); + + out.append(" assumed_state: "); + out.append(YESNO(this->assumed_state)); + out.append("\n"); + + out.append(" supports_open: "); + out.append(YESNO(this->supports_open)); + out.append("\n"); + + out.append(" requires_code: "); + out.append(YESNO(this->requires_code)); + out.append("\n"); + + out.append(" code_format: "); + out.append("'").append(this->code_format).append("'"); + out.append("\n"); + + out.append(" device_id: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void LockStateResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("LockStateResponse {\n"); + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" state: "); + out.append(proto_enum_to_string(this->state)); + out.append("\n"); + + out.append(" device_id: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void LockCommandRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("LockCommandRequest {\n"); + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" command: "); + out.append(proto_enum_to_string(this->command)); + out.append("\n"); + + out.append(" has_code: "); + out.append(YESNO(this->has_code)); + out.append("\n"); + + out.append(" code: "); + out.append("'").append(this->code).append("'"); + out.append("\n"); + out.append("}"); +} +#endif +#ifdef USE_BUTTON +void ListEntitiesButtonResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ListEntitiesButtonResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" unique_id: "); + out.append("'").append(this->unique_id).append("'"); + out.append("\n"); + + out.append(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); + + out.append(" device_class: "); + out.append("'").append(this->device_class).append("'"); + out.append("\n"); + + out.append(" device_id: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void ButtonCommandRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ButtonCommandRequest {\n"); + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +#endif +#ifdef USE_MEDIA_PLAYER +void MediaPlayerSupportedFormat::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("MediaPlayerSupportedFormat {\n"); + out.append(" format: "); + out.append("'").append(this->format).append("'"); + out.append("\n"); + + out.append(" sample_rate: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->sample_rate); + out.append(buffer); + out.append("\n"); + + out.append(" num_channels: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->num_channels); + out.append(buffer); + out.append("\n"); + + out.append(" purpose: "); + out.append(proto_enum_to_string(this->purpose)); + out.append("\n"); + + out.append(" sample_bytes: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->sample_bytes); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void ListEntitiesMediaPlayerResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ListEntitiesMediaPlayerResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" unique_id: "); + out.append("'").append(this->unique_id).append("'"); + out.append("\n"); + + out.append(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); + + out.append(" supports_pause: "); + out.append(YESNO(this->supports_pause)); + out.append("\n"); + + for (const auto &it : this->supported_formats) { + out.append(" supported_formats: "); + it.dump_to(out); + out.append("\n"); + } + + out.append(" device_id: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void MediaPlayerStateResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("MediaPlayerStateResponse {\n"); + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" state: "); + out.append(proto_enum_to_string(this->state)); + out.append("\n"); + + out.append(" volume: "); + snprintf(buffer, sizeof(buffer), "%g", this->volume); + out.append(buffer); + out.append("\n"); + + out.append(" muted: "); + out.append(YESNO(this->muted)); + out.append("\n"); + + out.append(" device_id: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void MediaPlayerCommandRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("MediaPlayerCommandRequest {\n"); + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" has_command: "); + out.append(YESNO(this->has_command)); + out.append("\n"); + + out.append(" command: "); + out.append(proto_enum_to_string(this->command)); + out.append("\n"); + + out.append(" has_volume: "); + out.append(YESNO(this->has_volume)); + out.append("\n"); + + out.append(" volume: "); + snprintf(buffer, sizeof(buffer), "%g", this->volume); + out.append(buffer); + out.append("\n"); + + out.append(" has_media_url: "); + out.append(YESNO(this->has_media_url)); + out.append("\n"); + + out.append(" media_url: "); + out.append("'").append(this->media_url).append("'"); + out.append("\n"); + + out.append(" has_announcement: "); + out.append(YESNO(this->has_announcement)); + out.append("\n"); + + out.append(" announcement: "); + out.append(YESNO(this->announcement)); + out.append("\n"); + out.append("}"); +} +#endif +#ifdef USE_BLUETOOTH_PROXY +void SubscribeBluetoothLEAdvertisementsRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("SubscribeBluetoothLEAdvertisementsRequest {\n"); + out.append(" flags: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->flags); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void BluetoothServiceData::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothServiceData {\n"); + out.append(" uuid: "); + out.append("'").append(this->uuid).append("'"); + out.append("\n"); + + for (const auto &it : this->legacy_data) { + out.append(" legacy_data: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, it); + out.append(buffer); + out.append("\n"); + } + + out.append(" data: "); + out.append(format_hex_pretty(this->data)); + out.append("\n"); + out.append("}"); +} +void BluetoothLEAdvertisementResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothLEAdvertisementResponse {\n"); + out.append(" address: "); + snprintf(buffer, sizeof(buffer), "%llu", this->address); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append(format_hex_pretty(this->name)); + out.append("\n"); + + out.append(" rssi: "); + snprintf(buffer, sizeof(buffer), "%" PRId32, this->rssi); + out.append(buffer); + out.append("\n"); + + for (const auto &it : this->service_uuids) { + out.append(" service_uuids: "); + out.append("'").append(it).append("'"); + out.append("\n"); + } + + for (const auto &it : this->service_data) { + out.append(" service_data: "); + it.dump_to(out); + out.append("\n"); + } + + for (const auto &it : this->manufacturer_data) { + out.append(" manufacturer_data: "); + it.dump_to(out); + out.append("\n"); + } + + out.append(" address_type: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->address_type); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void BluetoothLERawAdvertisement::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothLERawAdvertisement {\n"); + out.append(" address: "); + snprintf(buffer, sizeof(buffer), "%llu", this->address); + out.append(buffer); + out.append("\n"); + + out.append(" rssi: "); + snprintf(buffer, sizeof(buffer), "%" PRId32, this->rssi); + out.append(buffer); + out.append("\n"); + + out.append(" address_type: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->address_type); + out.append(buffer); + out.append("\n"); + + out.append(" data: "); + out.append(format_hex_pretty(this->data)); + out.append("\n"); + out.append("}"); +} +void BluetoothLERawAdvertisementsResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothLERawAdvertisementsResponse {\n"); + for (const auto &it : this->advertisements) { + out.append(" advertisements: "); + it.dump_to(out); + out.append("\n"); + } + out.append("}"); +} +void BluetoothDeviceRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothDeviceRequest {\n"); + out.append(" address: "); + snprintf(buffer, sizeof(buffer), "%llu", this->address); + out.append(buffer); + out.append("\n"); + + out.append(" request_type: "); + out.append(proto_enum_to_string(this->request_type)); + out.append("\n"); + + out.append(" has_address_type: "); + out.append(YESNO(this->has_address_type)); + out.append("\n"); + + out.append(" address_type: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->address_type); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void BluetoothDeviceConnectionResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothDeviceConnectionResponse {\n"); + out.append(" address: "); + snprintf(buffer, sizeof(buffer), "%llu", this->address); + out.append(buffer); + out.append("\n"); + + out.append(" connected: "); + out.append(YESNO(this->connected)); + out.append("\n"); + + out.append(" mtu: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->mtu); + out.append(buffer); + out.append("\n"); + + out.append(" error: "); + snprintf(buffer, sizeof(buffer), "%" PRId32, this->error); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void BluetoothGATTGetServicesRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothGATTGetServicesRequest {\n"); + out.append(" address: "); + snprintf(buffer, sizeof(buffer), "%llu", this->address); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void BluetoothGATTDescriptor::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothGATTDescriptor {\n"); + for (const auto &it : this->uuid) { + out.append(" uuid: "); + snprintf(buffer, sizeof(buffer), "%llu", it); + out.append(buffer); + out.append("\n"); + } + + out.append(" handle: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void BluetoothGATTCharacteristic::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothGATTCharacteristic {\n"); + for (const auto &it : this->uuid) { + out.append(" uuid: "); + snprintf(buffer, sizeof(buffer), "%llu", it); + out.append(buffer); + out.append("\n"); + } + + out.append(" handle: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); + out.append(buffer); + out.append("\n"); + + out.append(" properties: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->properties); + out.append(buffer); + out.append("\n"); + + for (const auto &it : this->descriptors) { + out.append(" descriptors: "); + it.dump_to(out); + out.append("\n"); + } + out.append("}"); +} +void BluetoothGATTService::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothGATTService {\n"); + for (const auto &it : this->uuid) { + out.append(" uuid: "); + snprintf(buffer, sizeof(buffer), "%llu", it); + out.append(buffer); + out.append("\n"); + } + + out.append(" handle: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); + out.append(buffer); + out.append("\n"); + + for (const auto &it : this->characteristics) { + out.append(" characteristics: "); + it.dump_to(out); + out.append("\n"); + } + out.append("}"); +} +void BluetoothGATTGetServicesResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothGATTGetServicesResponse {\n"); + out.append(" address: "); + snprintf(buffer, sizeof(buffer), "%llu", this->address); + out.append(buffer); + out.append("\n"); + + for (const auto &it : this->services) { + out.append(" services: "); + it.dump_to(out); + out.append("\n"); + } + out.append("}"); +} +void BluetoothGATTGetServicesDoneResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothGATTGetServicesDoneResponse {\n"); + out.append(" address: "); + snprintf(buffer, sizeof(buffer), "%llu", this->address); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void BluetoothGATTReadRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothGATTReadRequest {\n"); + out.append(" address: "); + snprintf(buffer, sizeof(buffer), "%llu", this->address); + out.append(buffer); + out.append("\n"); + + out.append(" handle: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void BluetoothGATTReadResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothGATTReadResponse {\n"); + out.append(" address: "); + snprintf(buffer, sizeof(buffer), "%llu", this->address); + out.append(buffer); + out.append("\n"); + + out.append(" handle: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); + out.append(buffer); + out.append("\n"); + + out.append(" data: "); + out.append(format_hex_pretty(this->data)); + out.append("\n"); + out.append("}"); +} +void BluetoothGATTWriteRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothGATTWriteRequest {\n"); + out.append(" address: "); + snprintf(buffer, sizeof(buffer), "%llu", this->address); + out.append(buffer); + out.append("\n"); + + out.append(" handle: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); + out.append(buffer); + out.append("\n"); + + out.append(" response: "); + out.append(YESNO(this->response)); + out.append("\n"); + + out.append(" data: "); + out.append(format_hex_pretty(this->data)); + out.append("\n"); + out.append("}"); +} +void BluetoothGATTReadDescriptorRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothGATTReadDescriptorRequest {\n"); + out.append(" address: "); + snprintf(buffer, sizeof(buffer), "%llu", this->address); + out.append(buffer); + out.append("\n"); + + out.append(" handle: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void BluetoothGATTWriteDescriptorRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothGATTWriteDescriptorRequest {\n"); + out.append(" address: "); + snprintf(buffer, sizeof(buffer), "%llu", this->address); + out.append(buffer); + out.append("\n"); + + out.append(" handle: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); + out.append(buffer); + out.append("\n"); + + out.append(" data: "); + out.append(format_hex_pretty(this->data)); + out.append("\n"); + out.append("}"); +} +void BluetoothGATTNotifyRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothGATTNotifyRequest {\n"); + out.append(" address: "); + snprintf(buffer, sizeof(buffer), "%llu", this->address); + out.append(buffer); + out.append("\n"); + + out.append(" handle: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); + out.append(buffer); + out.append("\n"); + + out.append(" enable: "); + out.append(YESNO(this->enable)); + out.append("\n"); + out.append("}"); +} +void BluetoothGATTNotifyDataResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothGATTNotifyDataResponse {\n"); + out.append(" address: "); + snprintf(buffer, sizeof(buffer), "%llu", this->address); + out.append(buffer); + out.append("\n"); + + out.append(" handle: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); + out.append(buffer); + out.append("\n"); + + out.append(" data: "); + out.append(format_hex_pretty(this->data)); + out.append("\n"); + out.append("}"); +} +void SubscribeBluetoothConnectionsFreeRequest::dump_to(std::string &out) const { + out.append("SubscribeBluetoothConnectionsFreeRequest {}"); +} +void BluetoothConnectionsFreeResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothConnectionsFreeResponse {\n"); + out.append(" free: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->free); + out.append(buffer); + out.append("\n"); + + out.append(" limit: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->limit); + out.append(buffer); + out.append("\n"); + + for (const auto &it : this->allocated) { + out.append(" allocated: "); + snprintf(buffer, sizeof(buffer), "%llu", it); + out.append(buffer); + out.append("\n"); + } + out.append("}"); +} +void BluetoothGATTErrorResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothGATTErrorResponse {\n"); + out.append(" address: "); + snprintf(buffer, sizeof(buffer), "%llu", this->address); + out.append(buffer); + out.append("\n"); + + out.append(" handle: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); + out.append(buffer); + out.append("\n"); + + out.append(" error: "); + snprintf(buffer, sizeof(buffer), "%" PRId32, this->error); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void BluetoothGATTWriteResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothGATTWriteResponse {\n"); + out.append(" address: "); + snprintf(buffer, sizeof(buffer), "%llu", this->address); + out.append(buffer); + out.append("\n"); + + out.append(" handle: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void BluetoothGATTNotifyResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothGATTNotifyResponse {\n"); + out.append(" address: "); + snprintf(buffer, sizeof(buffer), "%llu", this->address); + out.append(buffer); + out.append("\n"); + + out.append(" handle: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void BluetoothDevicePairingResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothDevicePairingResponse {\n"); + out.append(" address: "); + snprintf(buffer, sizeof(buffer), "%llu", this->address); + out.append(buffer); + out.append("\n"); + + out.append(" paired: "); + out.append(YESNO(this->paired)); + out.append("\n"); + + out.append(" error: "); + snprintf(buffer, sizeof(buffer), "%" PRId32, this->error); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void BluetoothDeviceUnpairingResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothDeviceUnpairingResponse {\n"); + out.append(" address: "); + snprintf(buffer, sizeof(buffer), "%llu", this->address); + out.append(buffer); + out.append("\n"); + + out.append(" success: "); + out.append(YESNO(this->success)); + out.append("\n"); + + out.append(" error: "); + snprintf(buffer, sizeof(buffer), "%" PRId32, this->error); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void UnsubscribeBluetoothLEAdvertisementsRequest::dump_to(std::string &out) const { + out.append("UnsubscribeBluetoothLEAdvertisementsRequest {}"); +} +void BluetoothDeviceClearCacheResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothDeviceClearCacheResponse {\n"); + out.append(" address: "); + snprintf(buffer, sizeof(buffer), "%llu", this->address); + out.append(buffer); + out.append("\n"); + + out.append(" success: "); + out.append(YESNO(this->success)); + out.append("\n"); + + out.append(" error: "); + snprintf(buffer, sizeof(buffer), "%" PRId32, this->error); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void BluetoothScannerStateResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothScannerStateResponse {\n"); + out.append(" state: "); + out.append(proto_enum_to_string(this->state)); + out.append("\n"); + + out.append(" mode: "); + out.append(proto_enum_to_string(this->mode)); + out.append("\n"); + out.append("}"); +} +void BluetoothScannerSetModeRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothScannerSetModeRequest {\n"); + out.append(" mode: "); + out.append(proto_enum_to_string(this->mode)); + out.append("\n"); + out.append("}"); +} +#endif +#ifdef USE_VOICE_ASSISTANT +void SubscribeVoiceAssistantRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("SubscribeVoiceAssistantRequest {\n"); + out.append(" subscribe: "); + out.append(YESNO(this->subscribe)); + out.append("\n"); + + out.append(" flags: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->flags); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void VoiceAssistantAudioSettings::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("VoiceAssistantAudioSettings {\n"); + out.append(" noise_suppression_level: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->noise_suppression_level); + out.append(buffer); + out.append("\n"); + + out.append(" auto_gain: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->auto_gain); + out.append(buffer); + out.append("\n"); + + out.append(" volume_multiplier: "); + snprintf(buffer, sizeof(buffer), "%g", this->volume_multiplier); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void VoiceAssistantRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("VoiceAssistantRequest {\n"); + out.append(" start: "); + out.append(YESNO(this->start)); + out.append("\n"); + + out.append(" conversation_id: "); + out.append("'").append(this->conversation_id).append("'"); + out.append("\n"); + + out.append(" flags: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->flags); + out.append(buffer); + out.append("\n"); + + out.append(" audio_settings: "); + this->audio_settings.dump_to(out); + out.append("\n"); + + out.append(" wake_word_phrase: "); + out.append("'").append(this->wake_word_phrase).append("'"); + out.append("\n"); + out.append("}"); +} +void VoiceAssistantResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("VoiceAssistantResponse {\n"); + out.append(" port: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->port); + out.append(buffer); + out.append("\n"); + + out.append(" error: "); + out.append(YESNO(this->error)); + out.append("\n"); + out.append("}"); +} +void VoiceAssistantEventData::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("VoiceAssistantEventData {\n"); + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" value: "); + out.append("'").append(this->value).append("'"); + out.append("\n"); + out.append("}"); +} +void VoiceAssistantEventResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("VoiceAssistantEventResponse {\n"); + out.append(" event_type: "); + out.append(proto_enum_to_string(this->event_type)); + out.append("\n"); + + for (const auto &it : this->data) { + out.append(" data: "); + it.dump_to(out); + out.append("\n"); + } + out.append("}"); +} +void VoiceAssistantAudio::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("VoiceAssistantAudio {\n"); + out.append(" data: "); + out.append(format_hex_pretty(this->data)); + out.append("\n"); + + out.append(" end: "); + out.append(YESNO(this->end)); + out.append("\n"); + out.append("}"); +} +void VoiceAssistantTimerEventResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("VoiceAssistantTimerEventResponse {\n"); + out.append(" event_type: "); + out.append(proto_enum_to_string(this->event_type)); + out.append("\n"); + + out.append(" timer_id: "); + out.append("'").append(this->timer_id).append("'"); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" total_seconds: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->total_seconds); + out.append(buffer); + out.append("\n"); + + out.append(" seconds_left: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->seconds_left); + out.append(buffer); + out.append("\n"); + + out.append(" is_active: "); + out.append(YESNO(this->is_active)); + out.append("\n"); + out.append("}"); +} +void VoiceAssistantAnnounceRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("VoiceAssistantAnnounceRequest {\n"); + out.append(" media_id: "); + out.append("'").append(this->media_id).append("'"); + out.append("\n"); + + out.append(" text: "); + out.append("'").append(this->text).append("'"); + out.append("\n"); + + out.append(" preannounce_media_id: "); + out.append("'").append(this->preannounce_media_id).append("'"); + out.append("\n"); + + out.append(" start_conversation: "); + out.append(YESNO(this->start_conversation)); + out.append("\n"); + out.append("}"); +} +void VoiceAssistantAnnounceFinished::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("VoiceAssistantAnnounceFinished {\n"); + out.append(" success: "); + out.append(YESNO(this->success)); + out.append("\n"); + out.append("}"); +} +void VoiceAssistantWakeWord::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("VoiceAssistantWakeWord {\n"); + out.append(" id: "); + out.append("'").append(this->id).append("'"); + out.append("\n"); + + out.append(" wake_word: "); + out.append("'").append(this->wake_word).append("'"); + out.append("\n"); + + for (const auto &it : this->trained_languages) { + out.append(" trained_languages: "); + out.append("'").append(it).append("'"); + out.append("\n"); + } + out.append("}"); +} +void VoiceAssistantConfigurationRequest::dump_to(std::string &out) const { + out.append("VoiceAssistantConfigurationRequest {}"); +} +void VoiceAssistantConfigurationResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("VoiceAssistantConfigurationResponse {\n"); + for (const auto &it : this->available_wake_words) { + out.append(" available_wake_words: "); + it.dump_to(out); + out.append("\n"); + } + + for (const auto &it : this->active_wake_words) { + out.append(" active_wake_words: "); + out.append("'").append(it).append("'"); + out.append("\n"); + } + + out.append(" max_active_wake_words: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->max_active_wake_words); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void VoiceAssistantSetConfiguration::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("VoiceAssistantSetConfiguration {\n"); + for (const auto &it : this->active_wake_words) { + out.append(" active_wake_words: "); + out.append("'").append(it).append("'"); + out.append("\n"); + } + out.append("}"); +} +#endif +#ifdef USE_ALARM_CONTROL_PANEL +void ListEntitiesAlarmControlPanelResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ListEntitiesAlarmControlPanelResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" unique_id: "); + out.append("'").append(this->unique_id).append("'"); + out.append("\n"); + + out.append(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); + + out.append(" supported_features: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->supported_features); + out.append(buffer); + out.append("\n"); + + out.append(" requires_code: "); + out.append(YESNO(this->requires_code)); + out.append("\n"); + + out.append(" requires_code_to_arm: "); + out.append(YESNO(this->requires_code_to_arm)); + out.append("\n"); + + out.append(" device_id: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void AlarmControlPanelStateResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("AlarmControlPanelStateResponse {\n"); + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" state: "); + out.append(proto_enum_to_string(this->state)); + out.append("\n"); + + out.append(" device_id: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void AlarmControlPanelCommandRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("AlarmControlPanelCommandRequest {\n"); + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" command: "); + out.append(proto_enum_to_string(this->command)); + out.append("\n"); + + out.append(" code: "); + out.append("'").append(this->code).append("'"); + out.append("\n"); + out.append("}"); +} +#endif +#ifdef USE_TEXT +void ListEntitiesTextResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ListEntitiesTextResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" unique_id: "); + out.append("'").append(this->unique_id).append("'"); + out.append("\n"); + + out.append(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); + + out.append(" min_length: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->min_length); + out.append(buffer); + out.append("\n"); + + out.append(" max_length: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->max_length); + out.append(buffer); + out.append("\n"); + + out.append(" pattern: "); + out.append("'").append(this->pattern).append("'"); + out.append("\n"); + + out.append(" mode: "); + out.append(proto_enum_to_string(this->mode)); + out.append("\n"); + + out.append(" device_id: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void TextStateResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("TextStateResponse {\n"); + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" state: "); + out.append("'").append(this->state).append("'"); + out.append("\n"); + + out.append(" missing_state: "); + out.append(YESNO(this->missing_state)); + out.append("\n"); + + out.append(" device_id: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void TextCommandRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("TextCommandRequest {\n"); + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" state: "); + out.append("'").append(this->state).append("'"); + out.append("\n"); + out.append("}"); +} +#endif +#ifdef USE_DATETIME_DATE +void ListEntitiesDateResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ListEntitiesDateResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" unique_id: "); + out.append("'").append(this->unique_id).append("'"); + out.append("\n"); + + out.append(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); + + out.append(" device_id: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void DateStateResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("DateStateResponse {\n"); + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" missing_state: "); + out.append(YESNO(this->missing_state)); + out.append("\n"); + + out.append(" year: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->year); + out.append(buffer); + out.append("\n"); + + out.append(" month: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->month); + out.append(buffer); + out.append("\n"); + + out.append(" day: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->day); + out.append(buffer); + out.append("\n"); + + out.append(" device_id: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void DateCommandRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("DateCommandRequest {\n"); + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" year: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->year); + out.append(buffer); + out.append("\n"); + + out.append(" month: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->month); + out.append(buffer); + out.append("\n"); + + out.append(" day: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->day); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +#endif +#ifdef USE_DATETIME_TIME +void ListEntitiesTimeResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ListEntitiesTimeResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" unique_id: "); + out.append("'").append(this->unique_id).append("'"); + out.append("\n"); + + out.append(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); + + out.append(" device_id: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void TimeStateResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("TimeStateResponse {\n"); + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" missing_state: "); + out.append(YESNO(this->missing_state)); + out.append("\n"); + + out.append(" hour: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->hour); + out.append(buffer); + out.append("\n"); + + out.append(" minute: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->minute); + out.append(buffer); + out.append("\n"); + + out.append(" second: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->second); + out.append(buffer); + out.append("\n"); + + out.append(" device_id: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void TimeCommandRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("TimeCommandRequest {\n"); + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" hour: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->hour); + out.append(buffer); + out.append("\n"); + + out.append(" minute: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->minute); + out.append(buffer); + out.append("\n"); + + out.append(" second: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->second); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +#endif +#ifdef USE_EVENT +void ListEntitiesEventResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ListEntitiesEventResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" unique_id: "); + out.append("'").append(this->unique_id).append("'"); + out.append("\n"); + + out.append(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); + + out.append(" device_class: "); + out.append("'").append(this->device_class).append("'"); + out.append("\n"); + + for (const auto &it : this->event_types) { + out.append(" event_types: "); + out.append("'").append(it).append("'"); + out.append("\n"); + } + + out.append(" device_id: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void EventResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("EventResponse {\n"); + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" event_type: "); + out.append("'").append(this->event_type).append("'"); + out.append("\n"); + + out.append(" device_id: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +#endif +#ifdef USE_VALVE +void ListEntitiesValveResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ListEntitiesValveResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" unique_id: "); + out.append("'").append(this->unique_id).append("'"); + out.append("\n"); + + out.append(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); + + out.append(" device_class: "); + out.append("'").append(this->device_class).append("'"); + out.append("\n"); + + out.append(" assumed_state: "); + out.append(YESNO(this->assumed_state)); + out.append("\n"); + + out.append(" supports_position: "); + out.append(YESNO(this->supports_position)); + out.append("\n"); + + out.append(" supports_stop: "); + out.append(YESNO(this->supports_stop)); + out.append("\n"); + + out.append(" device_id: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void ValveStateResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ValveStateResponse {\n"); + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" position: "); + snprintf(buffer, sizeof(buffer), "%g", this->position); + out.append(buffer); + out.append("\n"); + + out.append(" current_operation: "); + out.append(proto_enum_to_string(this->current_operation)); + out.append("\n"); + + out.append(" device_id: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void ValveCommandRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ValveCommandRequest {\n"); + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" has_position: "); + out.append(YESNO(this->has_position)); + out.append("\n"); + + out.append(" position: "); + snprintf(buffer, sizeof(buffer), "%g", this->position); + out.append(buffer); + out.append("\n"); + + out.append(" stop: "); + out.append(YESNO(this->stop)); + out.append("\n"); + out.append("}"); +} +#endif +#ifdef USE_DATETIME_DATETIME +void ListEntitiesDateTimeResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ListEntitiesDateTimeResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" unique_id: "); + out.append("'").append(this->unique_id).append("'"); + out.append("\n"); + + out.append(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); + + out.append(" device_id: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void DateTimeStateResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("DateTimeStateResponse {\n"); + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" missing_state: "); + out.append(YESNO(this->missing_state)); + out.append("\n"); + + out.append(" epoch_seconds: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->epoch_seconds); + out.append(buffer); + out.append("\n"); + + out.append(" device_id: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void DateTimeCommandRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("DateTimeCommandRequest {\n"); + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" epoch_seconds: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->epoch_seconds); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +#endif +#ifdef USE_UPDATE +void ListEntitiesUpdateResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ListEntitiesUpdateResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" unique_id: "); + out.append("'").append(this->unique_id).append("'"); + out.append("\n"); + + out.append(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); + + out.append(" device_class: "); + out.append("'").append(this->device_class).append("'"); + out.append("\n"); + + out.append(" device_id: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void UpdateStateResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("UpdateStateResponse {\n"); + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" missing_state: "); + out.append(YESNO(this->missing_state)); + out.append("\n"); + + out.append(" in_progress: "); + out.append(YESNO(this->in_progress)); + out.append("\n"); + + out.append(" has_progress: "); + out.append(YESNO(this->has_progress)); + out.append("\n"); + + out.append(" progress: "); + snprintf(buffer, sizeof(buffer), "%g", this->progress); + out.append(buffer); + out.append("\n"); + + out.append(" current_version: "); + out.append("'").append(this->current_version).append("'"); + out.append("\n"); + + out.append(" latest_version: "); + out.append("'").append(this->latest_version).append("'"); + out.append("\n"); + + out.append(" title: "); + out.append("'").append(this->title).append("'"); + out.append("\n"); + + out.append(" release_summary: "); + out.append("'").append(this->release_summary).append("'"); + out.append("\n"); + + out.append(" release_url: "); + out.append("'").append(this->release_url).append("'"); + out.append("\n"); + + out.append(" device_id: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +void UpdateCommandRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("UpdateCommandRequest {\n"); + out.append(" key: "); + snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); + out.append(buffer); + out.append("\n"); + + out.append(" command: "); + out.append(proto_enum_to_string(this->command)); + out.append("\n"); + out.append("}"); +} +#endif + +} // namespace api +} // namespace esphome + +#endif // HAS_PROTO_MESSAGE_DUMP diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index dacb23c12b..92dd90053b 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -14,7 +14,7 @@ void APIServerConnectionBase::log_send_message_(const char *name, const std::str } #endif -bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) { +void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) { switch (msg_type) { case 1: { HelloRequest msg; @@ -106,50 +106,50 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, this->on_subscribe_logs_request(msg); break; } - case 30: { #ifdef USE_COVER + case 30: { CoverCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_cover_command_request: %s", msg.dump().c_str()); #endif this->on_cover_command_request(msg); -#endif break; } - case 31: { +#endif #ifdef USE_FAN + case 31: { FanCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_fan_command_request: %s", msg.dump().c_str()); #endif this->on_fan_command_request(msg); -#endif break; } - case 32: { +#endif #ifdef USE_LIGHT + case 32: { LightCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_light_command_request: %s", msg.dump().c_str()); #endif this->on_light_command_request(msg); -#endif break; } - case 33: { +#endif #ifdef USE_SWITCH + case 33: { SwitchCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_switch_command_request: %s", msg.dump().c_str()); #endif this->on_switch_command_request(msg); -#endif break; } +#endif case 34: { SubscribeHomeassistantServicesRequest msg; msg.decode(msg_data, msg_size); @@ -204,395 +204,394 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, this->on_execute_service_request(msg); break; } +#ifdef USE_CAMERA case 45: { -#ifdef USE_ESP32_CAMERA CameraImageRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_camera_image_request: %s", msg.dump().c_str()); #endif this->on_camera_image_request(msg); -#endif break; } - case 48: { +#endif #ifdef USE_CLIMATE + case 48: { ClimateCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_climate_command_request: %s", msg.dump().c_str()); #endif this->on_climate_command_request(msg); -#endif break; } - case 51: { +#endif #ifdef USE_NUMBER + case 51: { NumberCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_number_command_request: %s", msg.dump().c_str()); #endif this->on_number_command_request(msg); -#endif break; } - case 54: { +#endif #ifdef USE_SELECT + case 54: { SelectCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_select_command_request: %s", msg.dump().c_str()); #endif this->on_select_command_request(msg); -#endif break; } - case 57: { +#endif #ifdef USE_SIREN + case 57: { SirenCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_siren_command_request: %s", msg.dump().c_str()); #endif this->on_siren_command_request(msg); -#endif break; } - case 60: { +#endif #ifdef USE_LOCK + case 60: { LockCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_lock_command_request: %s", msg.dump().c_str()); #endif this->on_lock_command_request(msg); -#endif break; } - case 62: { +#endif #ifdef USE_BUTTON + case 62: { ButtonCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_button_command_request: %s", msg.dump().c_str()); #endif this->on_button_command_request(msg); -#endif break; } - case 65: { +#endif #ifdef USE_MEDIA_PLAYER + case 65: { MediaPlayerCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_media_player_command_request: %s", msg.dump().c_str()); #endif this->on_media_player_command_request(msg); -#endif break; } - case 66: { +#endif #ifdef USE_BLUETOOTH_PROXY + case 66: { SubscribeBluetoothLEAdvertisementsRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_subscribe_bluetooth_le_advertisements_request: %s", msg.dump().c_str()); #endif this->on_subscribe_bluetooth_le_advertisements_request(msg); -#endif break; } - case 68: { +#endif #ifdef USE_BLUETOOTH_PROXY + case 68: { BluetoothDeviceRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_bluetooth_device_request: %s", msg.dump().c_str()); #endif this->on_bluetooth_device_request(msg); -#endif break; } - case 70: { +#endif #ifdef USE_BLUETOOTH_PROXY + case 70: { BluetoothGATTGetServicesRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_bluetooth_gatt_get_services_request: %s", msg.dump().c_str()); #endif this->on_bluetooth_gatt_get_services_request(msg); -#endif break; } - case 73: { +#endif #ifdef USE_BLUETOOTH_PROXY + case 73: { BluetoothGATTReadRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_bluetooth_gatt_read_request: %s", msg.dump().c_str()); #endif this->on_bluetooth_gatt_read_request(msg); -#endif break; } - case 75: { +#endif #ifdef USE_BLUETOOTH_PROXY + case 75: { BluetoothGATTWriteRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_bluetooth_gatt_write_request: %s", msg.dump().c_str()); #endif this->on_bluetooth_gatt_write_request(msg); -#endif break; } - case 76: { +#endif #ifdef USE_BLUETOOTH_PROXY + case 76: { BluetoothGATTReadDescriptorRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_bluetooth_gatt_read_descriptor_request: %s", msg.dump().c_str()); #endif this->on_bluetooth_gatt_read_descriptor_request(msg); -#endif break; } - case 77: { +#endif #ifdef USE_BLUETOOTH_PROXY + case 77: { BluetoothGATTWriteDescriptorRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_bluetooth_gatt_write_descriptor_request: %s", msg.dump().c_str()); #endif this->on_bluetooth_gatt_write_descriptor_request(msg); -#endif break; } - case 78: { +#endif #ifdef USE_BLUETOOTH_PROXY + case 78: { BluetoothGATTNotifyRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_bluetooth_gatt_notify_request: %s", msg.dump().c_str()); #endif this->on_bluetooth_gatt_notify_request(msg); -#endif break; } - case 80: { +#endif #ifdef USE_BLUETOOTH_PROXY + case 80: { SubscribeBluetoothConnectionsFreeRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_subscribe_bluetooth_connections_free_request: %s", msg.dump().c_str()); #endif this->on_subscribe_bluetooth_connections_free_request(msg); -#endif break; } - case 87: { +#endif #ifdef USE_BLUETOOTH_PROXY + case 87: { UnsubscribeBluetoothLEAdvertisementsRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_unsubscribe_bluetooth_le_advertisements_request: %s", msg.dump().c_str()); #endif this->on_unsubscribe_bluetooth_le_advertisements_request(msg); -#endif break; } - case 89: { +#endif #ifdef USE_VOICE_ASSISTANT + case 89: { SubscribeVoiceAssistantRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_subscribe_voice_assistant_request: %s", msg.dump().c_str()); #endif this->on_subscribe_voice_assistant_request(msg); -#endif break; } - case 91: { +#endif #ifdef USE_VOICE_ASSISTANT + case 91: { VoiceAssistantResponse msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_voice_assistant_response: %s", msg.dump().c_str()); #endif this->on_voice_assistant_response(msg); -#endif break; } - case 92: { +#endif #ifdef USE_VOICE_ASSISTANT + case 92: { VoiceAssistantEventResponse msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_voice_assistant_event_response: %s", msg.dump().c_str()); #endif this->on_voice_assistant_event_response(msg); -#endif break; } - case 96: { +#endif #ifdef USE_ALARM_CONTROL_PANEL + case 96: { AlarmControlPanelCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_alarm_control_panel_command_request: %s", msg.dump().c_str()); #endif this->on_alarm_control_panel_command_request(msg); -#endif break; } - case 99: { +#endif #ifdef USE_TEXT + case 99: { TextCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_text_command_request: %s", msg.dump().c_str()); #endif this->on_text_command_request(msg); -#endif break; } - case 102: { +#endif #ifdef USE_DATETIME_DATE + case 102: { DateCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_date_command_request: %s", msg.dump().c_str()); #endif this->on_date_command_request(msg); -#endif break; } - case 105: { +#endif #ifdef USE_DATETIME_TIME + case 105: { TimeCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_time_command_request: %s", msg.dump().c_str()); #endif this->on_time_command_request(msg); -#endif break; } - case 106: { +#endif #ifdef USE_VOICE_ASSISTANT + case 106: { VoiceAssistantAudio msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_voice_assistant_audio: %s", msg.dump().c_str()); #endif this->on_voice_assistant_audio(msg); -#endif break; } - case 111: { +#endif #ifdef USE_VALVE + case 111: { ValveCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_valve_command_request: %s", msg.dump().c_str()); #endif this->on_valve_command_request(msg); -#endif break; } - case 114: { +#endif #ifdef USE_DATETIME_DATETIME + case 114: { DateTimeCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_date_time_command_request: %s", msg.dump().c_str()); #endif this->on_date_time_command_request(msg); -#endif break; } - case 115: { +#endif #ifdef USE_VOICE_ASSISTANT + case 115: { VoiceAssistantTimerEventResponse msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_voice_assistant_timer_event_response: %s", msg.dump().c_str()); #endif this->on_voice_assistant_timer_event_response(msg); -#endif break; } - case 118: { +#endif #ifdef USE_UPDATE + case 118: { UpdateCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_update_command_request: %s", msg.dump().c_str()); #endif this->on_update_command_request(msg); -#endif break; } - case 119: { +#endif #ifdef USE_VOICE_ASSISTANT + case 119: { VoiceAssistantAnnounceRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_voice_assistant_announce_request: %s", msg.dump().c_str()); #endif this->on_voice_assistant_announce_request(msg); -#endif break; } - case 121: { +#endif #ifdef USE_VOICE_ASSISTANT + case 121: { VoiceAssistantConfigurationRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_voice_assistant_configuration_request: %s", msg.dump().c_str()); #endif this->on_voice_assistant_configuration_request(msg); -#endif break; } - case 123: { +#endif #ifdef USE_VOICE_ASSISTANT + case 123: { VoiceAssistantSetConfiguration msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_voice_assistant_set_configuration: %s", msg.dump().c_str()); #endif this->on_voice_assistant_set_configuration(msg); -#endif break; } - case 124: { +#endif #ifdef USE_API_NOISE + case 124: { NoiseEncryptionSetKeyRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_noise_encryption_set_key_request: %s", msg.dump().c_str()); #endif this->on_noise_encryption_set_key_request(msg); -#endif break; } - case 127: { +#endif #ifdef USE_BLUETOOTH_PROXY + case 127: { BluetoothScannerSetModeRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_bluetooth_scanner_set_mode_request: %s", msg.dump().c_str()); #endif this->on_bluetooth_scanner_set_mode_request(msg); -#endif break; } +#endif default: - return false; + break; } - return true; } void APIServerConnection::on_hello_request(const HelloRequest &msg) { @@ -620,544 +619,300 @@ void APIServerConnection::on_ping_request(const PingRequest &msg) { } } void APIServerConnection::on_device_info_request(const DeviceInfoRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; - } - DeviceInfoResponse ret = this->device_info(msg); - if (!this->send_message(ret)) { - this->on_fatal_error(); + if (this->check_connection_setup_()) { + DeviceInfoResponse ret = this->device_info(msg); + if (!this->send_message(ret)) { + this->on_fatal_error(); + } } } void APIServerConnection::on_list_entities_request(const ListEntitiesRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->list_entities(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->list_entities(msg); } void APIServerConnection::on_subscribe_states_request(const SubscribeStatesRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->subscribe_states(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->subscribe_states(msg); } void APIServerConnection::on_subscribe_logs_request(const SubscribeLogsRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->subscribe_logs(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->subscribe_logs(msg); } void APIServerConnection::on_subscribe_homeassistant_services_request( const SubscribeHomeassistantServicesRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->subscribe_homeassistant_services(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->subscribe_homeassistant_services(msg); } void APIServerConnection::on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->subscribe_home_assistant_states(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->subscribe_home_assistant_states(msg); } void APIServerConnection::on_get_time_request(const GetTimeRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; - } - GetTimeResponse ret = this->get_time(msg); - if (!this->send_message(ret)) { - this->on_fatal_error(); + if (this->check_connection_setup_()) { + GetTimeResponse ret = this->get_time(msg); + if (!this->send_message(ret)) { + this->on_fatal_error(); + } } } void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->execute_service(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->execute_service(msg); } #ifdef USE_API_NOISE void APIServerConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; - } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - NoiseEncryptionSetKeyResponse ret = this->noise_encryption_set_key(msg); - if (!this->send_message(ret)) { - this->on_fatal_error(); + if (this->check_authenticated_()) { + NoiseEncryptionSetKeyResponse ret = this->noise_encryption_set_key(msg); + if (!this->send_message(ret)) { + this->on_fatal_error(); + } } } #endif #ifdef USE_BUTTON void APIServerConnection::on_button_command_request(const ButtonCommandRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->button_command(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->button_command(msg); } #endif -#ifdef USE_ESP32_CAMERA +#ifdef USE_CAMERA void APIServerConnection::on_camera_image_request(const CameraImageRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->camera_image(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->camera_image(msg); } #endif #ifdef USE_CLIMATE void APIServerConnection::on_climate_command_request(const ClimateCommandRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->climate_command(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->climate_command(msg); } #endif #ifdef USE_COVER void APIServerConnection::on_cover_command_request(const CoverCommandRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->cover_command(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->cover_command(msg); } #endif #ifdef USE_DATETIME_DATE void APIServerConnection::on_date_command_request(const DateCommandRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->date_command(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->date_command(msg); } #endif #ifdef USE_DATETIME_DATETIME void APIServerConnection::on_date_time_command_request(const DateTimeCommandRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->datetime_command(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->datetime_command(msg); } #endif #ifdef USE_FAN void APIServerConnection::on_fan_command_request(const FanCommandRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->fan_command(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->fan_command(msg); } #endif #ifdef USE_LIGHT void APIServerConnection::on_light_command_request(const LightCommandRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->light_command(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->light_command(msg); } #endif #ifdef USE_LOCK void APIServerConnection::on_lock_command_request(const LockCommandRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->lock_command(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->lock_command(msg); } #endif #ifdef USE_MEDIA_PLAYER void APIServerConnection::on_media_player_command_request(const MediaPlayerCommandRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->media_player_command(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->media_player_command(msg); } #endif #ifdef USE_NUMBER void APIServerConnection::on_number_command_request(const NumberCommandRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->number_command(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->number_command(msg); } #endif #ifdef USE_SELECT void APIServerConnection::on_select_command_request(const SelectCommandRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->select_command(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->select_command(msg); } #endif #ifdef USE_SIREN void APIServerConnection::on_siren_command_request(const SirenCommandRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->siren_command(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->siren_command(msg); } #endif #ifdef USE_SWITCH void APIServerConnection::on_switch_command_request(const SwitchCommandRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->switch_command(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->switch_command(msg); } #endif #ifdef USE_TEXT void APIServerConnection::on_text_command_request(const TextCommandRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->text_command(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->text_command(msg); } #endif #ifdef USE_DATETIME_TIME void APIServerConnection::on_time_command_request(const TimeCommandRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->time_command(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->time_command(msg); } #endif #ifdef USE_UPDATE void APIServerConnection::on_update_command_request(const UpdateCommandRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->update_command(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->update_command(msg); } #endif #ifdef USE_VALVE void APIServerConnection::on_valve_command_request(const ValveCommandRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->valve_command(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->valve_command(msg); } #endif #ifdef USE_BLUETOOTH_PROXY void APIServerConnection::on_subscribe_bluetooth_le_advertisements_request( const SubscribeBluetoothLEAdvertisementsRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->subscribe_bluetooth_le_advertisements(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->subscribe_bluetooth_le_advertisements(msg); } #endif #ifdef USE_BLUETOOTH_PROXY void APIServerConnection::on_bluetooth_device_request(const BluetoothDeviceRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->bluetooth_device_request(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->bluetooth_device_request(msg); } #endif #ifdef USE_BLUETOOTH_PROXY void APIServerConnection::on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->bluetooth_gatt_get_services(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->bluetooth_gatt_get_services(msg); } #endif #ifdef USE_BLUETOOTH_PROXY void APIServerConnection::on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->bluetooth_gatt_read(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->bluetooth_gatt_read(msg); } #endif #ifdef USE_BLUETOOTH_PROXY void APIServerConnection::on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->bluetooth_gatt_write(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->bluetooth_gatt_write(msg); } #endif #ifdef USE_BLUETOOTH_PROXY void APIServerConnection::on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->bluetooth_gatt_read_descriptor(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->bluetooth_gatt_read_descriptor(msg); } #endif #ifdef USE_BLUETOOTH_PROXY void APIServerConnection::on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->bluetooth_gatt_write_descriptor(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->bluetooth_gatt_write_descriptor(msg); } #endif #ifdef USE_BLUETOOTH_PROXY void APIServerConnection::on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->bluetooth_gatt_notify(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->bluetooth_gatt_notify(msg); } #endif #ifdef USE_BLUETOOTH_PROXY void APIServerConnection::on_subscribe_bluetooth_connections_free_request( const SubscribeBluetoothConnectionsFreeRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; - } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - BluetoothConnectionsFreeResponse ret = this->subscribe_bluetooth_connections_free(msg); - if (!this->send_message(ret)) { - this->on_fatal_error(); + if (this->check_authenticated_()) { + BluetoothConnectionsFreeResponse ret = this->subscribe_bluetooth_connections_free(msg); + if (!this->send_message(ret)) { + this->on_fatal_error(); + } } } #endif #ifdef USE_BLUETOOTH_PROXY void APIServerConnection::on_unsubscribe_bluetooth_le_advertisements_request( const UnsubscribeBluetoothLEAdvertisementsRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->unsubscribe_bluetooth_le_advertisements(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->unsubscribe_bluetooth_le_advertisements(msg); } #endif #ifdef USE_BLUETOOTH_PROXY void APIServerConnection::on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->bluetooth_scanner_set_mode(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->bluetooth_scanner_set_mode(msg); } #endif #ifdef USE_VOICE_ASSISTANT void APIServerConnection::on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->subscribe_voice_assistant(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->subscribe_voice_assistant(msg); } #endif #ifdef USE_VOICE_ASSISTANT void APIServerConnection::on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; - } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - VoiceAssistantConfigurationResponse ret = this->voice_assistant_get_configuration(msg); - if (!this->send_message(ret)) { - this->on_fatal_error(); + if (this->check_authenticated_()) { + VoiceAssistantConfigurationResponse ret = this->voice_assistant_get_configuration(msg); + if (!this->send_message(ret)) { + this->on_fatal_error(); + } } } #endif #ifdef USE_VOICE_ASSISTANT void APIServerConnection::on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->voice_assistant_set_configuration(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->voice_assistant_set_configuration(msg); } #endif #ifdef USE_ALARM_CONTROL_PANEL void APIServerConnection::on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return; + if (this->check_authenticated_()) { + this->alarm_control_panel_command(msg); } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return; - } - this->alarm_control_panel_command(msg); } #endif diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index b2be314aaf..458f8ec81b 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -2,9 +2,10 @@ // See script/api_protobuf/api_protobuf.py #pragma once -#include "api_pb2.h" #include "esphome/core/defines.h" +#include "api_pb2.h" + namespace esphome { namespace api { @@ -19,7 +20,7 @@ class APIServerConnectionBase : public ProtoService { template bool send_message(const T &msg) { #ifdef HAS_PROTO_MESSAGE_DUMP - this->log_send_message_(T::message_name(), msg.dump()); + this->log_send_message_(msg.message_name(), msg.dump()); #endif return this->send_message_(msg, T::MESSAGE_TYPE); } @@ -70,7 +71,7 @@ class APIServerConnectionBase : public ProtoService { virtual void on_execute_service_request(const ExecuteServiceRequest &value){}; -#ifdef USE_ESP32_CAMERA +#ifdef USE_CAMERA virtual void on_camera_image_request(const CameraImageRequest &value){}; #endif @@ -199,7 +200,7 @@ class APIServerConnectionBase : public ProtoService { virtual void on_update_command_request(const UpdateCommandRequest &value){}; #endif protected: - bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override; + void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override; }; class APIServerConnection : public APIServerConnectionBase { @@ -222,7 +223,7 @@ class APIServerConnection : public APIServerConnectionBase { #ifdef USE_BUTTON virtual void button_command(const ButtonCommandRequest &msg) = 0; #endif -#ifdef USE_ESP32_CAMERA +#ifdef USE_CAMERA virtual void camera_image(const CameraImageRequest &msg) = 0; #endif #ifdef USE_CLIMATE @@ -339,7 +340,7 @@ class APIServerConnection : public APIServerConnectionBase { #ifdef USE_BUTTON void on_button_command_request(const ButtonCommandRequest &msg) override; #endif -#ifdef USE_ESP32_CAMERA +#ifdef USE_CAMERA void on_camera_image_request(const CameraImageRequest &msg) override; #endif #ifdef USE_CLIMATE diff --git a/esphome/components/api/api_pb2_size.h b/esphome/components/api/api_pb2_size.h index e591a7350f..f371be13a5 100644 --- a/esphome/components/api/api_pb2_size.h +++ b/esphome/components/api/api_pb2_size.h @@ -316,15 +316,13 @@ class ProtoSize { /** * @brief Calculates and adds the size of a nested message field to the total message size * - * This templated version directly takes a message object, calculates its size internally, + * This version takes a ProtoMessage object, calculates its size internally, * and updates the total_size reference. This eliminates the need for a temporary variable * at the call site. * - * @tparam MessageType The type of the nested message (inferred from parameter) * @param message The nested message object */ - template - static inline void add_message_object(uint32_t &total_size, uint32_t field_id_size, const MessageType &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; message.calculate_size(nested_size); diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 17c83c54f1..0915746381 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -24,6 +24,14 @@ static const char *const TAG = "api"; // APIServer 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 empty_user_services{}; + +const std::vector &get_empty_user_services_instance() { return empty_user_services; } +#endif + APIServer::APIServer() { global_api_server = this; // Pre-allocate shared write buffer @@ -47,6 +55,11 @@ void APIServer::setup() { } #endif + // Schedule reboot if no clients connect within timeout + if (this->reboot_timeout_ != 0) { + this->schedule_reboot_timeout_(); + } + this->socket_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections if (this->socket_ == nullptr) { ESP_LOGW(TAG, "Could not create socket"); @@ -91,34 +104,42 @@ void APIServer::setup() { #ifdef USE_LOGGER if (logger::global_logger != nullptr) { - logger::global_logger->add_on_log_callback([this](int level, const char *tag, const char *message) { - if (this->shutting_down_) { - // Don't try to send logs during shutdown - // as it could result in a recursion and - // we would be filling a buffer we are trying to clear - return; - } - for (auto &c : this->clients_) { - if (!c->remove_) - c->try_send_log_message(level, tag, message); - } - }); - } -#endif - - this->last_connected_ = millis(); - -#ifdef USE_ESP32_CAMERA - if (esp32_camera::global_esp32_camera != nullptr && !esp32_camera::global_esp32_camera->is_internal()) { - esp32_camera::global_esp32_camera->add_image_callback( - [this](const std::shared_ptr &image) { + logger::global_logger->add_on_log_callback( + [this](int level, const char *tag, const char *message, size_t message_len) { + if (this->shutting_down_) { + // Don't try to send logs during shutdown + // as it could result in a recursion and + // we would be filling a buffer we are trying to clear + return; + } for (auto &c : this->clients_) { - if (!c->remove_) - c->set_camera_state(image); + if (!c->flags_.remove) + c->try_send_log_message(level, tag, message, message_len); } }); } #endif + +#ifdef USE_CAMERA + if (camera::Camera::instance() != nullptr && !camera::Camera::instance()->is_internal()) { + camera::Camera::instance()->add_image_callback([this](const std::shared_ptr &image) { + for (auto &c : this->clients_) { + if (!c->flags_.remove) + c->set_camera_state(image); + } + }); + } +#endif +} + +void APIServer::schedule_reboot_timeout_() { + this->status_set_warning(); + this->set_timeout("api_reboot", this->reboot_timeout_, []() { + if (!global_api_server->is_connected()) { + ESP_LOGE(TAG, "No clients; rebooting"); + App.reboot(); + } + }); } void APIServer::loop() { @@ -130,51 +151,63 @@ void APIServer::loop() { auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len); if (!sock) break; - ESP_LOGD(TAG, "Accepted %s", sock->getpeername().c_str()); + ESP_LOGD(TAG, "Accept %s", sock->getpeername().c_str()); auto *conn = new APIConnection(std::move(sock), this); this->clients_.emplace_back(conn); conn->start(); + + // Clear warning status and cancel reboot when first client connects + if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) { + this->status_clear_warning(); + this->cancel_timeout("api_reboot"); + } } } + if (this->clients_.empty()) { + return; + } + // Process clients and remove disconnected ones in a single pass - if (!this->clients_.empty()) { - size_t client_index = 0; - while (client_index < this->clients_.size()) { - auto &client = this->clients_[client_index]; - - if (client->remove_) { - // Handle disconnection - this->client_disconnected_trigger_->trigger(client->client_info_, client->client_peername_); - ESP_LOGV(TAG, "Removing connection to %s", client->client_info_.c_str()); - - // Swap with the last element and pop (avoids expensive vector shifts) - if (client_index < this->clients_.size() - 1) { - std::swap(this->clients_[client_index], this->clients_.back()); - } - this->clients_.pop_back(); - // Don't increment client_index since we need to process the swapped element - } else { - // Process active client - client->loop(); - client_index++; // Move to next client - } + // Check network connectivity once for all clients + if (!network::is_connected()) { + // Network is down - disconnect all clients + for (auto &client : this->clients_) { + client->on_fatal_error(); + ESP_LOGW(TAG, "%s: Network down; disconnect", client->get_client_combined_info().c_str()); } + // Continue to process and clean up the clients below } - if (this->reboot_timeout_ != 0) { - const uint32_t now = millis(); - if (!this->is_connected()) { - if (now - this->last_connected_ > this->reboot_timeout_) { - ESP_LOGE(TAG, "No client connected; rebooting"); - App.reboot(); - } - this->status_set_warning(); - } else { - this->last_connected_ = now; - this->status_clear_warning(); + size_t client_index = 0; + while (client_index < this->clients_.size()) { + auto &client = this->clients_[client_index]; + + if (!client->flags_.remove) { + // Common case: process active client + client->loop(); + client_index++; + continue; } + + // Rare case: handle disconnection +#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER + this->client_disconnected_trigger_->trigger(client->client_info_, client->client_peername_); +#endif + ESP_LOGV(TAG, "Remove connection %s", client->client_info_.c_str()); + + // Swap with the last element and pop (avoids expensive vector shifts) + if (client_index < this->clients_.size() - 1) { + std::swap(this->clients_[client_index], this->clients_.back()); + } + this->clients_.pop_back(); + + // Schedule reboot when last client disconnects + if (this->clients_.empty() && this->reboot_timeout_ != 0) { + this->schedule_reboot_timeout_(); + } + // Don't increment client_index since we need to process the swapped element } } @@ -193,6 +226,7 @@ void APIServer::dump_config() { #endif } +#ifdef USE_API_PASSWORD bool APIServer::uses_password() const { return !this->password_.empty(); } bool APIServer::check_password(const std::string &password) const { @@ -223,192 +257,129 @@ bool APIServer::check_password(const std::string &password) const { return result == 0; } +#endif void APIServer::handle_disconnect(APIConnection *conn) {} +// Macro for entities without extra parameters +#define API_DISPATCH_UPDATE(entity_type, entity_name) \ + void APIServer::on_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \ + if (obj->is_internal()) \ + return; \ + for (auto &c : this->clients_) \ + c->send_##entity_name##_state(obj); \ + } + +// Macro for entities with extra parameters (but parameters not used in send) +#define API_DISPATCH_UPDATE_IGNORE_PARAMS(entity_type, entity_name, ...) \ + void APIServer::on_##entity_name##_update(entity_type *obj, __VA_ARGS__) { /* NOLINT(bugprone-macro-parentheses) */ \ + if (obj->is_internal()) \ + return; \ + for (auto &c : this->clients_) \ + c->send_##entity_name##_state(obj); \ + } + #ifdef USE_BINARY_SENSOR -void APIServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_binary_sensor_state(obj); -} +API_DISPATCH_UPDATE(binary_sensor::BinarySensor, binary_sensor) #endif #ifdef USE_COVER -void APIServer::on_cover_update(cover::Cover *obj) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_cover_state(obj); -} +API_DISPATCH_UPDATE(cover::Cover, cover) #endif #ifdef USE_FAN -void APIServer::on_fan_update(fan::Fan *obj) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_fan_state(obj); -} +API_DISPATCH_UPDATE(fan::Fan, fan) #endif #ifdef USE_LIGHT -void APIServer::on_light_update(light::LightState *obj) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_light_state(obj); -} +API_DISPATCH_UPDATE(light::LightState, light) #endif #ifdef USE_SENSOR -void APIServer::on_sensor_update(sensor::Sensor *obj, float state) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_sensor_state(obj); -} +API_DISPATCH_UPDATE_IGNORE_PARAMS(sensor::Sensor, sensor, float state) #endif #ifdef USE_SWITCH -void APIServer::on_switch_update(switch_::Switch *obj, bool state) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_switch_state(obj); -} +API_DISPATCH_UPDATE_IGNORE_PARAMS(switch_::Switch, switch, bool state) #endif #ifdef USE_TEXT_SENSOR -void APIServer::on_text_sensor_update(text_sensor::TextSensor *obj, const std::string &state) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_text_sensor_state(obj); -} +API_DISPATCH_UPDATE_IGNORE_PARAMS(text_sensor::TextSensor, text_sensor, const std::string &state) #endif #ifdef USE_CLIMATE -void APIServer::on_climate_update(climate::Climate *obj) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_climate_state(obj); -} +API_DISPATCH_UPDATE(climate::Climate, climate) #endif #ifdef USE_NUMBER -void APIServer::on_number_update(number::Number *obj, float state) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_number_state(obj); -} +API_DISPATCH_UPDATE_IGNORE_PARAMS(number::Number, number, float state) #endif #ifdef USE_DATETIME_DATE -void APIServer::on_date_update(datetime::DateEntity *obj) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_date_state(obj); -} +API_DISPATCH_UPDATE(datetime::DateEntity, date) #endif #ifdef USE_DATETIME_TIME -void APIServer::on_time_update(datetime::TimeEntity *obj) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_time_state(obj); -} +API_DISPATCH_UPDATE(datetime::TimeEntity, time) #endif #ifdef USE_DATETIME_DATETIME -void APIServer::on_datetime_update(datetime::DateTimeEntity *obj) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_datetime_state(obj); -} +API_DISPATCH_UPDATE(datetime::DateTimeEntity, datetime) #endif #ifdef USE_TEXT -void APIServer::on_text_update(text::Text *obj, const std::string &state) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_text_state(obj); -} +API_DISPATCH_UPDATE_IGNORE_PARAMS(text::Text, text, const std::string &state) #endif #ifdef USE_SELECT -void APIServer::on_select_update(select::Select *obj, const std::string &state, size_t index) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_select_state(obj); -} +API_DISPATCH_UPDATE_IGNORE_PARAMS(select::Select, select, const std::string &state, size_t index) #endif #ifdef USE_LOCK -void APIServer::on_lock_update(lock::Lock *obj) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_lock_state(obj); -} +API_DISPATCH_UPDATE(lock::Lock, lock) #endif #ifdef USE_VALVE -void APIServer::on_valve_update(valve::Valve *obj) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_valve_state(obj); -} +API_DISPATCH_UPDATE(valve::Valve, valve) #endif #ifdef USE_MEDIA_PLAYER -void APIServer::on_media_player_update(media_player::MediaPlayer *obj) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_media_player_state(obj); -} +API_DISPATCH_UPDATE(media_player::MediaPlayer, media_player) #endif #ifdef USE_EVENT +// Event is a special case - it's the only entity that passes extra parameters to the send method void APIServer::on_event(event::Event *obj, const std::string &event_type) { + if (obj->is_internal()) + return; for (auto &c : this->clients_) c->send_event(obj, event_type); } #endif #ifdef USE_UPDATE +// Update is a special case - the method is called on_update, not on_update_update void APIServer::on_update(update::UpdateEntity *obj) { + if (obj->is_internal()) + return; for (auto &c : this->clients_) c->send_update_state(obj); } #endif #ifdef USE_ALARM_CONTROL_PANEL -void APIServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) { - if (obj->is_internal()) - return; - for (auto &c : this->clients_) - c->send_alarm_control_panel_state(obj); -} +API_DISPATCH_UPDATE(alarm_control_panel::AlarmControlPanel, alarm_control_panel) #endif float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; } void APIServer::set_port(uint16_t port) { this->port_ = port; } +#ifdef USE_API_PASSWORD void APIServer::set_password(const std::string &password) { this->password_ = password; } +#endif -void APIServer::set_batch_delay(uint32_t batch_delay) { this->batch_delay_ = batch_delay; } +void APIServer::set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = batch_delay; } void APIServer::send_homeassistant_service_call(const HomeassistantServiceResponse &call) { for (auto &client : this->clients_) { @@ -479,7 +450,7 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) { #ifdef USE_HOMEASSISTANT_TIME void APIServer::request_time() { for (auto &client : this->clients_) { - if (!client->remove_ && client->is_authenticated()) + if (!client->flags_.remove && client->is_authenticated()) client->send_time_request(); } } @@ -503,8 +474,8 @@ void APIServer::on_shutdown() { for (auto &c : this->clients_) { if (!c->send_message(DisconnectRequest())) { // If we can't send the disconnect request directly (tx_buffer full), - // schedule it in the batch so it will be sent with the 5ms timer - c->schedule_message_(nullptr, &APIConnection::try_send_disconnect_request, DisconnectRequest::MESSAGE_TYPE); + // 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); } } } diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 71e470d4f8..f34fd55974 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -25,6 +25,11 @@ struct SavedNoisePsk { } PACKED; // NOLINT #endif +#ifndef USE_API_YAML_SERVICES +// Forward declaration of helper function +const std::vector &get_empty_user_services_instance(); +#endif + class APIServer : public Component, public Controller { public: APIServer(); @@ -35,13 +40,15 @@ class APIServer : public Component, public Controller { void dump_config() override; void on_shutdown() override; bool teardown() override; +#ifdef USE_API_PASSWORD bool check_password(const std::string &password) const; bool uses_password() const; - void set_port(uint16_t port); void set_password(const std::string &password); +#endif + void set_port(uint16_t port); void set_reboot_timeout(uint32_t reboot_timeout); - void set_batch_delay(uint32_t batch_delay); - uint32_t get_batch_delay() const { return batch_delay_; } + void set_batch_delay(uint16_t batch_delay); + uint16_t get_batch_delay() const { return batch_delay_; } // Get reference to shared buffer for API connections std::vector &get_shared_buffer_ref() { return shared_write_buffer_; } @@ -54,7 +61,7 @@ class APIServer : public Component, public Controller { void handle_disconnect(APIConnection *conn); #ifdef USE_BINARY_SENSOR - void on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) override; + void on_binary_sensor_update(binary_sensor::BinarySensor *obj) override; #endif #ifdef USE_COVER void on_cover_update(cover::Cover *obj) override; @@ -105,7 +112,18 @@ class APIServer : public Component, public Controller { void on_media_player_update(media_player::MediaPlayer *obj) override; #endif void send_homeassistant_service_call(const HomeassistantServiceResponse &call); - void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } + void register_user_service(UserServiceDescriptor *descriptor) { +#ifdef USE_API_YAML_SERVICES + // 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>(); + } + this->user_services_->push_back(descriptor); +#endif + } #ifdef USE_HOMEASSISTANT_TIME void request_time(); #endif @@ -134,27 +152,63 @@ class APIServer : public Component, public Controller { void get_home_assistant_state(std::string entity_id, optional attribute, std::function f); const std::vector &get_state_subs() const; - const std::vector &get_user_services() const { return this->user_services_; } + const std::vector &get_user_services() const { +#ifdef USE_API_YAML_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 + } +#ifdef USE_API_CLIENT_CONNECTED_TRIGGER Trigger *get_client_connected_trigger() const { return this->client_connected_trigger_; } +#endif +#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER Trigger *get_client_disconnected_trigger() const { return this->client_disconnected_trigger_; } +#endif protected: - bool shutting_down_ = false; + void schedule_reboot_timeout_(); + // Pointers and pointer-like types first (4 bytes each) std::unique_ptr socket_ = nullptr; - uint16_t port_{6053}; +#ifdef USE_API_CLIENT_CONNECTED_TRIGGER + Trigger *client_connected_trigger_ = new Trigger(); +#endif +#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER + Trigger *client_disconnected_trigger_ = new Trigger(); +#endif + + // 4-byte aligned types uint32_t reboot_timeout_{300000}; - uint32_t batch_delay_{100}; - uint32_t last_connected_{0}; + + // Vectors and strings (12 bytes each on 32-bit) std::vector> clients_; +#ifdef USE_API_PASSWORD std::string password_; +#endif std::vector shared_write_buffer_; // Shared proto write buffer for all connections std::vector state_subs_; +#ifdef USE_API_YAML_SERVICES + // When services are defined in YAML, we know at compile time that services will be registered std::vector user_services_; - Trigger *client_connected_trigger_ = new Trigger(); - Trigger *client_disconnected_trigger_ = new Trigger(); +#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> user_services_; +#endif + + // Group smaller types together + uint16_t port_{6053}; + uint16_t batch_delay_{100}; + bool shutting_down_ = false; + // 5 bytes used, 3 bytes padding #ifdef USE_API_NOISE std::shared_ptr noise_ctx_ = std::make_shared(); diff --git a/esphome/components/api/client.py b/esphome/components/api/client.py index 24a6c5f19f..2d4bc37c89 100644 --- a/esphome/components/api/client.py +++ b/esphome/components/api/client.py @@ -4,9 +4,15 @@ import asyncio from datetime import datetime import logging from typing import TYPE_CHECKING, Any +import warnings -from aioesphomeapi import APIClient, parse_log_message -from aioesphomeapi.log_runner import async_run +# Suppress protobuf version warnings +with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", category=UserWarning, message=".*Protobuf gencode version.*" + ) + from aioesphomeapi import APIClient, parse_log_message + from aioesphomeapi.log_runner import async_run from esphome.const import CONF_KEY, CONF_PASSWORD, CONF_PORT, __version__ from esphome.core import CORE diff --git a/esphome/components/api/list_entities.cpp b/esphome/components/api/list_entities.cpp index ceee3f00b8..60814e359d 100644 --- a/esphome/components/api/list_entities.cpp +++ b/esphome/components/api/list_entities.cpp @@ -1,6 +1,7 @@ #include "list_entities.h" #ifdef USE_API #include "api_connection.h" +#include "api_pb2.h" #include "esphome/core/application.h" #include "esphome/core/log.h" #include "esphome/core/util.h" @@ -8,155 +9,85 @@ namespace esphome { namespace api { +// Generate entity handler implementations using macros #ifdef USE_BINARY_SENSOR -bool ListEntitiesIterator::on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) { - this->client_->send_binary_sensor_info(binary_sensor); - return true; -} +LIST_ENTITIES_HANDLER(binary_sensor, binary_sensor::BinarySensor, ListEntitiesBinarySensorResponse) #endif #ifdef USE_COVER -bool ListEntitiesIterator::on_cover(cover::Cover *cover) { - this->client_->send_cover_info(cover); - return true; -} +LIST_ENTITIES_HANDLER(cover, cover::Cover, ListEntitiesCoverResponse) #endif #ifdef USE_FAN -bool ListEntitiesIterator::on_fan(fan::Fan *fan) { - this->client_->send_fan_info(fan); - return true; -} +LIST_ENTITIES_HANDLER(fan, fan::Fan, ListEntitiesFanResponse) #endif #ifdef USE_LIGHT -bool ListEntitiesIterator::on_light(light::LightState *light) { - this->client_->send_light_info(light); - return true; -} +LIST_ENTITIES_HANDLER(light, light::LightState, ListEntitiesLightResponse) #endif #ifdef USE_SENSOR -bool ListEntitiesIterator::on_sensor(sensor::Sensor *sensor) { - this->client_->send_sensor_info(sensor); - return true; -} +LIST_ENTITIES_HANDLER(sensor, sensor::Sensor, ListEntitiesSensorResponse) #endif #ifdef USE_SWITCH -bool ListEntitiesIterator::on_switch(switch_::Switch *a_switch) { - this->client_->send_switch_info(a_switch); - return true; -} +LIST_ENTITIES_HANDLER(switch, switch_::Switch, ListEntitiesSwitchResponse) #endif #ifdef USE_BUTTON -bool ListEntitiesIterator::on_button(button::Button *button) { - this->client_->send_button_info(button); - return true; -} +LIST_ENTITIES_HANDLER(button, button::Button, ListEntitiesButtonResponse) #endif #ifdef USE_TEXT_SENSOR -bool ListEntitiesIterator::on_text_sensor(text_sensor::TextSensor *text_sensor) { - this->client_->send_text_sensor_info(text_sensor); - return true; -} +LIST_ENTITIES_HANDLER(text_sensor, text_sensor::TextSensor, ListEntitiesTextSensorResponse) #endif #ifdef USE_LOCK -bool ListEntitiesIterator::on_lock(lock::Lock *a_lock) { - this->client_->send_lock_info(a_lock); - return true; -} +LIST_ENTITIES_HANDLER(lock, lock::Lock, ListEntitiesLockResponse) #endif #ifdef USE_VALVE -bool ListEntitiesIterator::on_valve(valve::Valve *valve) { - this->client_->send_valve_info(valve); - return true; -} +LIST_ENTITIES_HANDLER(valve, valve::Valve, ListEntitiesValveResponse) +#endif +#ifdef USE_CAMERA +LIST_ENTITIES_HANDLER(camera, camera::Camera, ListEntitiesCameraResponse) +#endif +#ifdef USE_CLIMATE +LIST_ENTITIES_HANDLER(climate, climate::Climate, ListEntitiesClimateResponse) +#endif +#ifdef USE_NUMBER +LIST_ENTITIES_HANDLER(number, number::Number, ListEntitiesNumberResponse) +#endif +#ifdef USE_DATETIME_DATE +LIST_ENTITIES_HANDLER(date, datetime::DateEntity, ListEntitiesDateResponse) +#endif +#ifdef USE_DATETIME_TIME +LIST_ENTITIES_HANDLER(time, datetime::TimeEntity, ListEntitiesTimeResponse) +#endif +#ifdef USE_DATETIME_DATETIME +LIST_ENTITIES_HANDLER(datetime, datetime::DateTimeEntity, ListEntitiesDateTimeResponse) +#endif +#ifdef USE_TEXT +LIST_ENTITIES_HANDLER(text, text::Text, ListEntitiesTextResponse) +#endif +#ifdef USE_SELECT +LIST_ENTITIES_HANDLER(select, select::Select, ListEntitiesSelectResponse) +#endif +#ifdef USE_MEDIA_PLAYER +LIST_ENTITIES_HANDLER(media_player, media_player::MediaPlayer, ListEntitiesMediaPlayerResponse) +#endif +#ifdef USE_ALARM_CONTROL_PANEL +LIST_ENTITIES_HANDLER(alarm_control_panel, alarm_control_panel::AlarmControlPanel, + ListEntitiesAlarmControlPanelResponse) +#endif +#ifdef USE_EVENT +LIST_ENTITIES_HANDLER(event, event::Event, ListEntitiesEventResponse) +#endif +#ifdef USE_UPDATE +LIST_ENTITIES_HANDLER(update, update::UpdateEntity, ListEntitiesUpdateResponse) #endif +// Special cases that don't follow the pattern bool ListEntitiesIterator::on_end() { return this->client_->send_list_info_done(); } + ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(client) {} + bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) { auto resp = service->encode_list_service_response(); return this->client_->send_message(resp); } -#ifdef USE_ESP32_CAMERA -bool ListEntitiesIterator::on_camera(esp32_camera::ESP32Camera *camera) { - this->client_->send_camera_info(camera); - return true; -} -#endif - -#ifdef USE_CLIMATE -bool ListEntitiesIterator::on_climate(climate::Climate *climate) { - this->client_->send_climate_info(climate); - return true; -} -#endif - -#ifdef USE_NUMBER -bool ListEntitiesIterator::on_number(number::Number *number) { - this->client_->send_number_info(number); - return true; -} -#endif - -#ifdef USE_DATETIME_DATE -bool ListEntitiesIterator::on_date(datetime::DateEntity *date) { - this->client_->send_date_info(date); - return true; -} -#endif - -#ifdef USE_DATETIME_TIME -bool ListEntitiesIterator::on_time(datetime::TimeEntity *time) { - this->client_->send_time_info(time); - return true; -} -#endif - -#ifdef USE_DATETIME_DATETIME -bool ListEntitiesIterator::on_datetime(datetime::DateTimeEntity *datetime) { - this->client_->send_datetime_info(datetime); - return true; -} -#endif - -#ifdef USE_TEXT -bool ListEntitiesIterator::on_text(text::Text *text) { - this->client_->send_text_info(text); - return true; -} -#endif - -#ifdef USE_SELECT -bool ListEntitiesIterator::on_select(select::Select *select) { - this->client_->send_select_info(select); - return true; -} -#endif - -#ifdef USE_MEDIA_PLAYER -bool ListEntitiesIterator::on_media_player(media_player::MediaPlayer *media_player) { - this->client_->send_media_player_info(media_player); - return true; -} -#endif -#ifdef USE_ALARM_CONTROL_PANEL -bool ListEntitiesIterator::on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { - this->client_->send_alarm_control_panel_info(a_alarm_control_panel); - return true; -} -#endif -#ifdef USE_EVENT -bool ListEntitiesIterator::on_event(event::Event *event) { - this->client_->send_event_info(event); - return true; -} -#endif -#ifdef USE_UPDATE -bool ListEntitiesIterator::on_update(update::UpdateEntity *update) { - this->client_->send_update_info(update); - return true; -} -#endif - } // namespace api } // namespace esphome #endif diff --git a/esphome/components/api/list_entities.h b/esphome/components/api/list_entities.h index e77f21c7a1..4c83ca0935 100644 --- a/esphome/components/api/list_entities.h +++ b/esphome/components/api/list_entities.h @@ -9,75 +9,83 @@ namespace api { class APIConnection; +// Macro for generating ListEntitiesIterator handlers +// Calls schedule_message_ with try_send_*_info +#define LIST_ENTITIES_HANDLER(entity_type, EntityClass, ResponseType) \ + bool ListEntitiesIterator::on_##entity_type(EntityClass *entity) { /* NOLINT(bugprone-macro-parentheses) */ \ + return this->client_->schedule_message_(entity, &APIConnection::try_send_##entity_type##_info, \ + ResponseType::MESSAGE_TYPE); \ + } + class ListEntitiesIterator : public ComponentIterator { public: ListEntitiesIterator(APIConnection *client); #ifdef USE_BINARY_SENSOR - bool on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) override; + bool on_binary_sensor(binary_sensor::BinarySensor *entity) override; #endif #ifdef USE_COVER - bool on_cover(cover::Cover *cover) override; + bool on_cover(cover::Cover *entity) override; #endif #ifdef USE_FAN - bool on_fan(fan::Fan *fan) override; + bool on_fan(fan::Fan *entity) override; #endif #ifdef USE_LIGHT - bool on_light(light::LightState *light) override; + bool on_light(light::LightState *entity) override; #endif #ifdef USE_SENSOR - bool on_sensor(sensor::Sensor *sensor) override; + bool on_sensor(sensor::Sensor *entity) override; #endif #ifdef USE_SWITCH - bool on_switch(switch_::Switch *a_switch) override; + bool on_switch(switch_::Switch *entity) override; #endif #ifdef USE_BUTTON - bool on_button(button::Button *button) override; + bool on_button(button::Button *entity) override; #endif #ifdef USE_TEXT_SENSOR - bool on_text_sensor(text_sensor::TextSensor *text_sensor) override; + bool on_text_sensor(text_sensor::TextSensor *entity) override; #endif bool on_service(UserServiceDescriptor *service) override; -#ifdef USE_ESP32_CAMERA - bool on_camera(esp32_camera::ESP32Camera *camera) override; +#ifdef USE_CAMERA + bool on_camera(camera::Camera *entity) override; #endif #ifdef USE_CLIMATE - bool on_climate(climate::Climate *climate) override; + bool on_climate(climate::Climate *entity) override; #endif #ifdef USE_NUMBER - bool on_number(number::Number *number) override; + bool on_number(number::Number *entity) override; #endif #ifdef USE_DATETIME_DATE - bool on_date(datetime::DateEntity *date) override; + bool on_date(datetime::DateEntity *entity) override; #endif #ifdef USE_DATETIME_TIME - bool on_time(datetime::TimeEntity *time) override; + bool on_time(datetime::TimeEntity *entity) override; #endif #ifdef USE_DATETIME_DATETIME - bool on_datetime(datetime::DateTimeEntity *datetime) override; + bool on_datetime(datetime::DateTimeEntity *entity) override; #endif #ifdef USE_TEXT - bool on_text(text::Text *text) override; + bool on_text(text::Text *entity) override; #endif #ifdef USE_SELECT - bool on_select(select::Select *select) override; + bool on_select(select::Select *entity) override; #endif #ifdef USE_LOCK - bool on_lock(lock::Lock *a_lock) override; + bool on_lock(lock::Lock *entity) override; #endif #ifdef USE_VALVE - bool on_valve(valve::Valve *valve) override; + bool on_valve(valve::Valve *entity) override; #endif #ifdef USE_MEDIA_PLAYER - bool on_media_player(media_player::MediaPlayer *media_player) override; + bool on_media_player(media_player::MediaPlayer *entity) override; #endif #ifdef USE_ALARM_CONTROL_PANEL - bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) override; + bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *entity) override; #endif #ifdef USE_EVENT - bool on_event(event::Event *event) override; + bool on_event(event::Event *entity) override; #endif #ifdef USE_UPDATE - bool on_update(update::UpdateEntity *update) override; + bool on_update(update::UpdateEntity *entity) override; #endif bool on_end() override; bool completed() { return this->state_ == IteratorState::NONE; } diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index eb0dbc151b..764bac2f39 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -327,12 +327,15 @@ class ProtoWriteBuffer { class ProtoMessage { public: virtual ~ProtoMessage() = default; - virtual void encode(ProtoWriteBuffer buffer) const = 0; + // Default implementation for messages with no fields + virtual void encode(ProtoWriteBuffer buffer) const {} void decode(const uint8_t *buffer, size_t length); - virtual void calculate_size(uint32_t &total_size) const = 0; + // Default implementation for messages with no fields + virtual void calculate_size(uint32_t &total_size) const {} #ifdef HAS_PROTO_MESSAGE_DUMP std::string dump() const; virtual void dump_to(std::string &out) const = 0; + virtual const char *message_name() const { return "unknown"; } #endif protected: @@ -361,7 +364,7 @@ class ProtoService { */ virtual ProtoWriteBuffer create_buffer(uint32_t reserve_size) = 0; virtual bool send_buffer(ProtoWriteBuffer buffer, uint16_t message_type) = 0; - virtual bool 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 bool send_message_(const ProtoMessage &msg, uint16_t message_type) { @@ -377,6 +380,26 @@ class ProtoService { // Send the buffer return this->send_buffer(buffer, message_type); } + + // Authentication helper methods + bool check_connection_setup_() { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return false; + } + return true; + } + + bool check_authenticated_() { + if (!this->check_connection_setup_()) { + return false; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return false; + } + return true; + } }; } // namespace api diff --git a/esphome/components/api/subscribe_state.cpp b/esphome/components/api/subscribe_state.cpp index 4180435fcc..12accf4613 100644 --- a/esphome/components/api/subscribe_state.cpp +++ b/esphome/components/api/subscribe_state.cpp @@ -6,73 +6,67 @@ namespace esphome { namespace api { +// Generate entity handler implementations using macros #ifdef USE_BINARY_SENSOR -bool InitialStateIterator::on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) { - return this->client_->send_binary_sensor_state(binary_sensor); -} +INITIAL_STATE_HANDLER(binary_sensor, binary_sensor::BinarySensor) #endif #ifdef USE_COVER -bool InitialStateIterator::on_cover(cover::Cover *cover) { return this->client_->send_cover_state(cover); } +INITIAL_STATE_HANDLER(cover, cover::Cover) #endif #ifdef USE_FAN -bool InitialStateIterator::on_fan(fan::Fan *fan) { return this->client_->send_fan_state(fan); } +INITIAL_STATE_HANDLER(fan, fan::Fan) #endif #ifdef USE_LIGHT -bool InitialStateIterator::on_light(light::LightState *light) { return this->client_->send_light_state(light); } +INITIAL_STATE_HANDLER(light, light::LightState) #endif #ifdef USE_SENSOR -bool InitialStateIterator::on_sensor(sensor::Sensor *sensor) { return this->client_->send_sensor_state(sensor); } +INITIAL_STATE_HANDLER(sensor, sensor::Sensor) #endif #ifdef USE_SWITCH -bool InitialStateIterator::on_switch(switch_::Switch *a_switch) { return this->client_->send_switch_state(a_switch); } +INITIAL_STATE_HANDLER(switch, switch_::Switch) #endif #ifdef USE_TEXT_SENSOR -bool InitialStateIterator::on_text_sensor(text_sensor::TextSensor *text_sensor) { - return this->client_->send_text_sensor_state(text_sensor); -} +INITIAL_STATE_HANDLER(text_sensor, text_sensor::TextSensor) #endif #ifdef USE_CLIMATE -bool InitialStateIterator::on_climate(climate::Climate *climate) { return this->client_->send_climate_state(climate); } +INITIAL_STATE_HANDLER(climate, climate::Climate) #endif #ifdef USE_NUMBER -bool InitialStateIterator::on_number(number::Number *number) { return this->client_->send_number_state(number); } +INITIAL_STATE_HANDLER(number, number::Number) #endif #ifdef USE_DATETIME_DATE -bool InitialStateIterator::on_date(datetime::DateEntity *date) { return this->client_->send_date_state(date); } +INITIAL_STATE_HANDLER(date, datetime::DateEntity) #endif #ifdef USE_DATETIME_TIME -bool InitialStateIterator::on_time(datetime::TimeEntity *time) { return this->client_->send_time_state(time); } +INITIAL_STATE_HANDLER(time, datetime::TimeEntity) #endif #ifdef USE_DATETIME_DATETIME -bool InitialStateIterator::on_datetime(datetime::DateTimeEntity *datetime) { - return this->client_->send_datetime_state(datetime); -} +INITIAL_STATE_HANDLER(datetime, datetime::DateTimeEntity) #endif #ifdef USE_TEXT -bool InitialStateIterator::on_text(text::Text *text) { return this->client_->send_text_state(text); } +INITIAL_STATE_HANDLER(text, text::Text) #endif #ifdef USE_SELECT -bool InitialStateIterator::on_select(select::Select *select) { return this->client_->send_select_state(select); } +INITIAL_STATE_HANDLER(select, select::Select) #endif #ifdef USE_LOCK -bool InitialStateIterator::on_lock(lock::Lock *a_lock) { return this->client_->send_lock_state(a_lock); } +INITIAL_STATE_HANDLER(lock, lock::Lock) #endif #ifdef USE_VALVE -bool InitialStateIterator::on_valve(valve::Valve *valve) { return this->client_->send_valve_state(valve); } +INITIAL_STATE_HANDLER(valve, valve::Valve) #endif #ifdef USE_MEDIA_PLAYER -bool InitialStateIterator::on_media_player(media_player::MediaPlayer *media_player) { - return this->client_->send_media_player_state(media_player); -} +INITIAL_STATE_HANDLER(media_player, media_player::MediaPlayer) #endif #ifdef USE_ALARM_CONTROL_PANEL -bool InitialStateIterator::on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { - return this->client_->send_alarm_control_panel_state(a_alarm_control_panel); -} +INITIAL_STATE_HANDLER(alarm_control_panel, alarm_control_panel::AlarmControlPanel) #endif #ifdef USE_UPDATE -bool InitialStateIterator::on_update(update::UpdateEntity *update) { return this->client_->send_update_state(update); } +INITIAL_STATE_HANDLER(update, update::UpdateEntity) #endif + +// Special cases (button and event) are already defined inline in subscribe_state.h + InitialStateIterator::InitialStateIterator(APIConnection *client) : client_(client) {} } // namespace api diff --git a/esphome/components/api/subscribe_state.h b/esphome/components/api/subscribe_state.h index 3966c97af5..2b7b508056 100644 --- a/esphome/components/api/subscribe_state.h +++ b/esphome/components/api/subscribe_state.h @@ -10,71 +10,78 @@ namespace api { class APIConnection; +// Macro for generating InitialStateIterator handlers +// Calls send_*_state +#define INITIAL_STATE_HANDLER(entity_type, EntityClass) \ + bool InitialStateIterator::on_##entity_type(EntityClass *entity) { /* NOLINT(bugprone-macro-parentheses) */ \ + return this->client_->send_##entity_type##_state(entity); \ + } + class InitialStateIterator : public ComponentIterator { public: InitialStateIterator(APIConnection *client); #ifdef USE_BINARY_SENSOR - bool on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) override; + bool on_binary_sensor(binary_sensor::BinarySensor *entity) override; #endif #ifdef USE_COVER - bool on_cover(cover::Cover *cover) override; + bool on_cover(cover::Cover *entity) override; #endif #ifdef USE_FAN - bool on_fan(fan::Fan *fan) override; + bool on_fan(fan::Fan *entity) override; #endif #ifdef USE_LIGHT - bool on_light(light::LightState *light) override; + bool on_light(light::LightState *entity) override; #endif #ifdef USE_SENSOR - bool on_sensor(sensor::Sensor *sensor) override; + bool on_sensor(sensor::Sensor *entity) override; #endif #ifdef USE_SWITCH - bool on_switch(switch_::Switch *a_switch) override; + bool on_switch(switch_::Switch *entity) override; #endif #ifdef USE_BUTTON bool on_button(button::Button *button) override { return true; }; #endif #ifdef USE_TEXT_SENSOR - bool on_text_sensor(text_sensor::TextSensor *text_sensor) override; + bool on_text_sensor(text_sensor::TextSensor *entity) override; #endif #ifdef USE_CLIMATE - bool on_climate(climate::Climate *climate) override; + bool on_climate(climate::Climate *entity) override; #endif #ifdef USE_NUMBER - bool on_number(number::Number *number) override; + bool on_number(number::Number *entity) override; #endif #ifdef USE_DATETIME_DATE - bool on_date(datetime::DateEntity *date) override; + bool on_date(datetime::DateEntity *entity) override; #endif #ifdef USE_DATETIME_TIME - bool on_time(datetime::TimeEntity *time) override; + bool on_time(datetime::TimeEntity *entity) override; #endif #ifdef USE_DATETIME_DATETIME - bool on_datetime(datetime::DateTimeEntity *datetime) override; + bool on_datetime(datetime::DateTimeEntity *entity) override; #endif #ifdef USE_TEXT - bool on_text(text::Text *text) override; + bool on_text(text::Text *entity) override; #endif #ifdef USE_SELECT - bool on_select(select::Select *select) override; + bool on_select(select::Select *entity) override; #endif #ifdef USE_LOCK - bool on_lock(lock::Lock *a_lock) override; + bool on_lock(lock::Lock *entity) override; #endif #ifdef USE_VALVE - bool on_valve(valve::Valve *valve) override; + bool on_valve(valve::Valve *entity) override; #endif #ifdef USE_MEDIA_PLAYER - bool on_media_player(media_player::MediaPlayer *media_player) override; + bool on_media_player(media_player::MediaPlayer *entity) override; #endif #ifdef USE_ALARM_CONTROL_PANEL - bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) override; + bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *entity) override; #endif #ifdef USE_EVENT bool on_event(event::Event *event) override { return true; }; #endif #ifdef USE_UPDATE - bool on_update(update::UpdateEntity *update) override; + bool on_update(update::UpdateEntity *entity) override; #endif bool completed() { return this->state_ == IteratorState::NONE; } diff --git a/esphome/components/as5600/as5600.h b/esphome/components/as5600/as5600.h index fbfd18db40..914a4431bd 100644 --- a/esphome/components/as5600/as5600.h +++ b/esphome/components/as5600/as5600.h @@ -50,7 +50,6 @@ class AS5600Component : public Component, public i2c::I2CDevice { void setup() override; void dump_config() override; /// HARDWARE_LATE setup priority - float get_setup_priority() const override { return setup_priority::DATA; } // configuration setters void set_dir_pin(InternalGPIOPin *pin) { this->dir_pin_ = pin; } diff --git a/esphome/components/async_tcp/__init__.py b/esphome/components/async_tcp/__init__.py index 99e250b6fc..29097ce1b6 100644 --- a/esphome/components/async_tcp/__init__.py +++ b/esphome/components/async_tcp/__init__.py @@ -5,6 +5,7 @@ from esphome.const import ( PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, + PLATFORM_LN882X, PLATFORM_RTL87XX, ) from esphome.core import CORE, coroutine_with_priority @@ -14,15 +15,23 @@ CODEOWNERS = ["@OttoWinter"] CONFIG_SCHEMA = cv.All( cv.Schema({}), cv.only_with_arduino, - cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX, PLATFORM_RTL87XX]), + cv.only_on( + [ + PLATFORM_ESP32, + PLATFORM_ESP8266, + PLATFORM_BK72XX, + PLATFORM_LN882X, + PLATFORM_RTL87XX, + ] + ), ) @coroutine_with_priority(200.0) async def to_code(config): if CORE.is_esp32 or CORE.is_libretiny: - # https://github.com/esphome/AsyncTCP/blob/master/library.json - cg.add_library("esphome/AsyncTCP-esphome", "2.1.4") + # https://github.com/ESP32Async/AsyncTCP + cg.add_library("ESP32Async/AsyncTCP", "3.4.4") elif CORE.is_esp8266: - # https://github.com/esphome/ESPAsyncTCP - cg.add_library("esphome/ESPAsyncTCP-esphome", "2.0.0") + # https://github.com/ESP32Async/ESPAsyncTCP + cg.add_library("ESP32Async/ESPAsyncTCP", "2.0.0") diff --git a/esphome/components/atc_mithermometer/atc_mithermometer.h b/esphome/components/atc_mithermometer/atc_mithermometer.h index 31fb77ac7f..d22e3f069b 100644 --- a/esphome/components/atc_mithermometer/atc_mithermometer.h +++ b/esphome/components/atc_mithermometer/atc_mithermometer.h @@ -25,7 +25,6 @@ class ATCMiThermometer : public Component, public esp32_ble_tracker::ESPBTDevice bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } diff --git a/esphome/components/atm90e32/atm90e32.cpp b/esphome/components/atm90e32/atm90e32.cpp index f05d462845..4669a59e39 100644 --- a/esphome/components/atm90e32/atm90e32.cpp +++ b/esphome/components/atm90e32/atm90e32.cpp @@ -1,6 +1,7 @@ #include "atm90e32.h" #include #include +#include #include "esphome/core/log.h" namespace esphome { @@ -848,7 +849,7 @@ uint16_t ATM90E32Component::calculate_voltage_threshold(int line_freq, uint16_t float nominal_voltage = (line_freq == 60) ? 120.0f : 220.0f; float target_voltage = nominal_voltage * multiplier; - float peak_01v = target_voltage * 100.0f * std::sqrt(2.0f); // convert RMS → peak, scale to 0.01V + float peak_01v = target_voltage * 100.0f * std::numbers::sqrt2_v; // convert RMS → peak, scale to 0.01V float divider = (2.0f * ugain) / 32768.0f; float threshold = peak_01v / divider; diff --git a/esphome/components/audio/audio_decoder.cpp b/esphome/components/audio/audio_decoder.cpp index c74b028c4b..90ba1aec1e 100644 --- a/esphome/components/audio/audio_decoder.cpp +++ b/esphome/components/audio/audio_decoder.cpp @@ -312,7 +312,7 @@ FileDecoderState AudioDecoder::decode_mp3_() { if (err) { switch (err) { case esp_audio_libs::helix_decoder::ERR_MP3_OUT_OF_MEMORY: - // Intentional fallthrough + [[fallthrough]]; case esp_audio_libs::helix_decoder::ERR_MP3_NULL_POINTER: return FileDecoderState::FAILED; break; diff --git a/esphome/components/audio/audio_transfer_buffer.cpp b/esphome/components/audio/audio_transfer_buffer.cpp index 1566884c3d..790cd62db0 100644 --- a/esphome/components/audio/audio_transfer_buffer.cpp +++ b/esphome/components/audio/audio_transfer_buffer.cpp @@ -86,7 +86,7 @@ bool AudioTransferBuffer::reallocate(size_t new_buffer_size) { bool AudioTransferBuffer::allocate_buffer_(size_t buffer_size) { this->buffer_size_ = buffer_size; - RAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator allocator; this->buffer_ = allocator.allocate(this->buffer_size_); if (this->buffer_ == nullptr) { @@ -101,7 +101,7 @@ bool AudioTransferBuffer::allocate_buffer_(size_t buffer_size) { void AudioTransferBuffer::deallocate_buffer_() { if (this->buffer_ != nullptr) { - RAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator allocator; allocator.deallocate(this->buffer_, this->buffer_size_); this->buffer_ = nullptr; this->data_start_ = nullptr; diff --git a/esphome/components/b_parasite/b_parasite.h b/esphome/components/b_parasite/b_parasite.h index 70ee4ab23c..7dd08968ec 100644 --- a/esphome/components/b_parasite/b_parasite.h +++ b/esphome/components/b_parasite/b_parasite.h @@ -16,7 +16,6 @@ class BParasite : public Component, public esp32_ble_tracker::ESPBTDeviceListene bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_battery_voltage(sensor::Sensor *battery_voltage) { battery_voltage_ = battery_voltage; } void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } diff --git a/esphome/components/bedjet/bedjet_hub.cpp b/esphome/components/bedjet/bedjet_hub.cpp index 7ebed2e78d..007ca1ca7d 100644 --- a/esphome/components/bedjet/bedjet_hub.cpp +++ b/esphome/components/bedjet/bedjet_hub.cpp @@ -480,7 +480,11 @@ void BedJetHub::set_clock(uint8_t hour, uint8_t minute) { /* Internal */ -void BedJetHub::loop() {} +void BedJetHub::loop() { + // Parent BLEClientNode has a loop() method, but this component uses + // polling via update() and BLE callbacks so loop isn't needed + this->disable_loop(); +} void BedJetHub::update() { this->dispatch_status_(); } void BedJetHub::dump_config() { diff --git a/esphome/components/bedjet/climate/bedjet_climate.cpp b/esphome/components/bedjet/climate/bedjet_climate.cpp index 854129f816..f22d312b5a 100644 --- a/esphome/components/bedjet/climate/bedjet_climate.cpp +++ b/esphome/components/bedjet/climate/bedjet_climate.cpp @@ -83,7 +83,11 @@ void BedJetClimate::reset_state_() { this->publish_state(); } -void BedJetClimate::loop() {} +void BedJetClimate::loop() { + // This component is controlled via the parent BedJetHub + // Empty loop not needed, disable to save CPU cycles + this->disable_loop(); +} void BedJetClimate::control(const ClimateCall &call) { ESP_LOGD(TAG, "Received BedJetClimate::control"); diff --git a/esphome/components/beken_spi_led_strip/led_strip.cpp b/esphome/components/beken_spi_led_strip/led_strip.cpp index d4585d7d36..17b2dd1808 100644 --- a/esphome/components/beken_spi_led_strip/led_strip.cpp +++ b/esphome/components/beken_spi_led_strip/led_strip.cpp @@ -7,11 +7,13 @@ extern "C" { #include "rtos_pub.h" -#include "spi.h" +// rtos_pub.h must be included before the rest of the includes + #include "arm_arch.h" #include "general_dma_pub.h" #include "gpio_pub.h" #include "icu_pub.h" +#include "spi.h" #undef SPI_DAT #undef SPI_BASE }; @@ -124,7 +126,7 @@ void BekenSPILEDStripLightOutput::setup() { size_t buffer_size = this->get_buffer_size_(); size_t dma_buffer_size = (buffer_size * 8) + (2 * 64); - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator allocator; this->buf_ = allocator.allocate(buffer_size); if (this->buf_ == nullptr) { ESP_LOGE(TAG, "Cannot allocate LED buffer!"); diff --git a/esphome/components/bh1750/bh1750.cpp b/esphome/components/bh1750/bh1750.cpp index 4b51794907..267a728fdd 100644 --- a/esphome/components/bh1750/bh1750.cpp +++ b/esphome/components/bh1750/bh1750.cpp @@ -50,7 +50,7 @@ void BH1750Sensor::read_lx_(BH1750Mode mode, uint8_t mtreg, const std::function< // turn on (after one-shot sensor automatically powers down) uint8_t turn_on = BH1750_COMMAND_POWER_ON; if (this->write(&turn_on, 1) != i2c::ERROR_OK) { - ESP_LOGW(TAG, "Turning on BH1750 failed"); + ESP_LOGW(TAG, "Power on failed"); f(NAN); return; } @@ -60,7 +60,7 @@ void BH1750Sensor::read_lx_(BH1750Mode mode, uint8_t mtreg, const std::function< uint8_t mtreg_hi = BH1750_COMMAND_MT_REG_HI | ((mtreg >> 5) & 0b111); uint8_t mtreg_lo = BH1750_COMMAND_MT_REG_LO | ((mtreg >> 0) & 0b11111); if (this->write(&mtreg_hi, 1) != i2c::ERROR_OK || this->write(&mtreg_lo, 1) != i2c::ERROR_OK) { - ESP_LOGW(TAG, "Setting measurement time for BH1750 failed"); + ESP_LOGW(TAG, "Set measurement time failed"); active_mtreg_ = 0; f(NAN); return; @@ -88,7 +88,7 @@ void BH1750Sensor::read_lx_(BH1750Mode mode, uint8_t mtreg, const std::function< return; } if (this->write(&cmd, 1) != i2c::ERROR_OK) { - ESP_LOGW(TAG, "Starting measurement for BH1750 failed"); + ESP_LOGW(TAG, "Start measurement failed"); f(NAN); return; } @@ -99,7 +99,7 @@ void BH1750Sensor::read_lx_(BH1750Mode mode, uint8_t mtreg, const std::function< this->set_timeout("read", meas_time, [this, mode, mtreg, f]() { uint16_t raw_value; if (this->read(reinterpret_cast(&raw_value), 2) != i2c::ERROR_OK) { - ESP_LOGW(TAG, "Reading BH1750 data failed"); + ESP_LOGW(TAG, "Read data failed"); f(NAN); return; } @@ -156,7 +156,7 @@ void BH1750Sensor::update() { this->publish_state(NAN); return; } - ESP_LOGD(TAG, "'%s': Got illuminance=%.1flx", this->get_name().c_str(), val); + ESP_LOGD(TAG, "'%s': Illuminance=%.1flx", this->get_name().c_str(), val); this->status_clear_warning(); this->publish_state(val); }); diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index ec1c4e8a0c..c97de6d5e5 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -1,7 +1,10 @@ +from logging import getLogger + from esphome import automation, core from esphome.automation import Condition, maybe_simple_id import esphome.codegen as cg from esphome.components import mqtt, web_server +from esphome.components.const import CONF_ON_STATE_CHANGE import esphome.config_validation as cv from esphome.const import ( CONF_DELAY, @@ -57,8 +60,8 @@ from esphome.const import ( DEVICE_CLASS_WINDOW, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity from esphome.util import Registry CODEOWNERS = ["@esphome/core"] @@ -98,6 +101,7 @@ IS_PLATFORM_COMPONENT = True CONF_TIME_OFF = "time_off" CONF_TIME_ON = "time_on" +CONF_TRIGGER_ON_INITIAL_STATE = "trigger_on_initial_state" DEFAULT_DELAY = "1s" DEFAULT_TIME_OFF = "100ms" @@ -127,15 +131,24 @@ MultiClickTriggerEvent = binary_sensor_ns.struct("MultiClickTriggerEvent") StateTrigger = binary_sensor_ns.class_( "StateTrigger", automation.Trigger.template(bool) ) +StateChangeTrigger = binary_sensor_ns.class_( + "StateChangeTrigger", + automation.Trigger.template(cg.optional.template(bool), cg.optional.template(bool)), +) + BinarySensorPublishAction = binary_sensor_ns.class_( "BinarySensorPublishAction", automation.Action ) +BinarySensorInvalidateAction = binary_sensor_ns.class_( + "BinarySensorInvalidateAction", automation.Action +) # Condition BinarySensorCondition = binary_sensor_ns.class_("BinarySensorCondition", Condition) # Filters Filter = binary_sensor_ns.class_("Filter") +TimeoutFilter = binary_sensor_ns.class_("TimeoutFilter", Filter, cg.Component) DelayedOnOffFilter = binary_sensor_ns.class_("DelayedOnOffFilter", Filter, cg.Component) DelayedOnFilter = binary_sensor_ns.class_("DelayedOnFilter", Filter, cg.Component) DelayedOffFilter = binary_sensor_ns.class_("DelayedOffFilter", Filter, cg.Component) @@ -144,6 +157,8 @@ AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter, cg.Compon LambdaFilter = binary_sensor_ns.class_("LambdaFilter", Filter) SettleFilter = binary_sensor_ns.class_("SettleFilter", Filter, cg.Component) +_LOGGER = getLogger(__name__) + FILTER_REGISTRY = Registry() validate_filters = cv.validate_registry("filter", FILTER_REGISTRY) @@ -157,6 +172,19 @@ async def invert_filter_to_code(config, filter_id): return cg.new_Pvariable(filter_id) +@register_filter( + "timeout", + TimeoutFilter, + cv.templatable(cv.positive_time_period_milliseconds), +) +async def timeout_filter_to_code(config, filter_id): + var = cg.new_Pvariable(filter_id) + await cg.register_component(var, {}) + template_ = await cg.templatable(config, [], cg.uint32) + cg.add(var.set_timeout_value(template_)) + return var + + @register_filter( "delayed_on_off", DelayedOnOffFilter, @@ -386,6 +414,14 @@ def validate_click_timing(value): return value +def validate_publish_initial_state(value): + value = cv.boolean(value) + _LOGGER.warning( + "The 'publish_initial_state' option has been replaced by 'trigger_on_initial_state' and will be removed in a future release" + ) + return value + + _BINARY_SENSOR_SCHEMA = ( cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) .extend(cv.MQTT_COMPONENT_SCHEMA) @@ -395,7 +431,12 @@ _BINARY_SENSOR_SCHEMA = ( cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id( mqtt.MQTTBinarySensorComponent ), - cv.Optional(CONF_PUBLISH_INITIAL_STATE): cv.boolean, + cv.Exclusive( + CONF_PUBLISH_INITIAL_STATE, CONF_TRIGGER_ON_INITIAL_STATE + ): validate_publish_initial_state, + cv.Exclusive( + CONF_TRIGGER_ON_INITIAL_STATE, CONF_TRIGGER_ON_INITIAL_STATE + ): cv.boolean, cv.Optional(CONF_DEVICE_CLASS): validate_device_class, cv.Optional(CONF_FILTERS): validate_filters, cv.Optional(CONF_ON_PRESS): automation.validate_automation( @@ -454,11 +495,19 @@ _BINARY_SENSOR_SCHEMA = ( cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger), } ), + cv.Optional(CONF_ON_STATE_CHANGE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateChangeTrigger), + } + ), } ) ) +_BINARY_SENSOR_SCHEMA.add_extra(entity_duplicate_validator("binary_sensor")) + + def binary_sensor_schema( class_: MockObjClass = cv.UNDEFINED, *, @@ -489,12 +538,14 @@ BINARY_SENSOR_SCHEMA.add_extra(cv.deprecated_schema_constant("binary_sensor")) async def setup_binary_sensor_core_(var, config): - await setup_entity(var, config) + await setup_entity(var, config, "binary_sensor") if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: cg.add(var.set_device_class(device_class)) - if publish_initial_state := config.get(CONF_PUBLISH_INITIAL_STATE): - cg.add(var.set_publish_initial_state(publish_initial_state)) + trigger = config.get(CONF_TRIGGER_ON_INITIAL_STATE, False) or config.get( + CONF_PUBLISH_INITIAL_STATE, False + ) + cg.add(var.set_trigger_on_initial_state(trigger)) if inverted := config.get(CONF_INVERTED): cg.add(var.set_inverted(inverted)) if filters_config := config.get(CONF_FILTERS): @@ -542,6 +593,17 @@ async def setup_binary_sensor_core_(var, config): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [(bool, "x")], conf) + for conf in config.get(CONF_ON_STATE_CHANGE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation( + trigger, + [ + (cg.optional.template(bool), "x_previous"), + (cg.optional.template(bool), "x"), + ], + conf, + ) + if mqtt_id := config.get(CONF_MQTT_ID): mqtt_ = cg.new_Pvariable(mqtt_id, var) await mqtt.register_mqtt_component(mqtt_, config) @@ -591,3 +653,18 @@ async def binary_sensor_is_off_to_code(config, condition_id, template_arg, args) async def to_code(config): cg.add_define("USE_BINARY_SENSOR") cg.add_global(binary_sensor_ns.using) + + +@automation.register_action( + "binary_sensor.invalidate_state", + BinarySensorInvalidateAction, + cv.maybe_simple_value( + { + cv.Required(CONF_ID): cv.use_id(BinarySensor), + }, + key=CONF_ID, + ), +) +async def binary_sensor_invalidate_state_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/binary_sensor/automation.h b/esphome/components/binary_sensor/automation.h index 12b07a05e3..b46436dc41 100644 --- a/esphome/components/binary_sensor/automation.h +++ b/esphome/components/binary_sensor/automation.h @@ -96,7 +96,7 @@ class MultiClickTrigger : public Trigger<>, public Component { : parent_(parent), timing_(std::move(timing)) {} void setup() override { - this->last_state_ = this->parent_->state; + this->last_state_ = this->parent_->get_state_default(false); auto f = std::bind(&MultiClickTrigger::on_state_, this, std::placeholders::_1); this->parent_->add_on_state_callback(f); } @@ -130,6 +130,14 @@ class StateTrigger : public Trigger { } }; +class StateChangeTrigger : public Trigger, optional > { + public: + explicit StateChangeTrigger(BinarySensor *parent) { + parent->add_full_state_callback( + [this](optional old_state, optional state) { this->trigger(old_state, state); }); + } +}; + template class BinarySensorCondition : public Condition { public: BinarySensorCondition(BinarySensor *parent, bool state) : parent_(parent), state_(state) {} @@ -154,5 +162,15 @@ template class BinarySensorPublishAction : public Action BinarySensor *sensor_; }; +template class BinarySensorInvalidateAction : public Action { + public: + explicit BinarySensorInvalidateAction(BinarySensor *sensor) : sensor_(sensor) {} + + void play(Ts... x) override { this->sensor_->invalidate_state(); } + + protected: + BinarySensor *sensor_; +}; + } // namespace binary_sensor } // namespace esphome diff --git a/esphome/components/binary_sensor/binary_sensor.cpp b/esphome/components/binary_sensor/binary_sensor.cpp index 20604a0b7e..02b83af552 100644 --- a/esphome/components/binary_sensor/binary_sensor.cpp +++ b/esphome/components/binary_sensor/binary_sensor.cpp @@ -7,42 +7,25 @@ namespace binary_sensor { static const char *const TAG = "binary_sensor"; -void BinarySensor::add_on_state_callback(std::function &&callback) { - this->state_callback_.add(std::move(callback)); -} - -void BinarySensor::publish_state(bool state) { - if (!this->publish_dedup_.next(state)) - return; +void BinarySensor::publish_state(bool new_state) { if (this->filter_list_ == nullptr) { - this->send_state_internal(state, false); + this->send_state_internal(new_state); } else { - this->filter_list_->input(state, false); + this->filter_list_->input(new_state); } } -void BinarySensor::publish_initial_state(bool state) { - if (!this->publish_dedup_.next(state)) - return; - if (this->filter_list_ == nullptr) { - this->send_state_internal(state, true); - } else { - this->filter_list_->input(state, true); +void BinarySensor::publish_initial_state(bool new_state) { + this->invalidate_state(); + this->publish_state(new_state); +} +void BinarySensor::send_state_internal(bool new_state) { + // copy the new state to the visible property for backwards compatibility, before any callbacks + this->state = new_state; + // Note that set_state_ de-dups and will only trigger callbacks if the state has actually changed + if (this->set_state_(new_state)) { + ESP_LOGD(TAG, "'%s': New state is %s", this->get_name().c_str(), ONOFF(new_state)); } } -void BinarySensor::send_state_internal(bool state, bool is_initial) { - if (is_initial) { - ESP_LOGD(TAG, "'%s': Sending initial state %s", this->get_name().c_str(), ONOFF(state)); - } else { - ESP_LOGD(TAG, "'%s': Sending state %s", this->get_name().c_str(), ONOFF(state)); - } - this->has_state_ = true; - this->state = state; - if (!is_initial || this->publish_initial_state_) { - this->state_callback_.call(state); - } -} - -BinarySensor::BinarySensor() : state(false) {} void BinarySensor::add_filter(Filter *filter) { filter->parent_ = this; @@ -60,7 +43,6 @@ void BinarySensor::add_filters(const std::vector &filters) { this->add_filter(filter); } } -bool BinarySensor::has_state() const { return this->has_state_; } bool BinarySensor::is_status_binary_sensor() const { return false; } } // namespace binary_sensor diff --git a/esphome/components/binary_sensor/binary_sensor.h b/esphome/components/binary_sensor/binary_sensor.h index 57cae9e2f5..d61be7a49b 100644 --- a/esphome/components/binary_sensor/binary_sensor.h +++ b/esphome/components/binary_sensor/binary_sensor.h @@ -1,6 +1,5 @@ #pragma once -#include "esphome/core/component.h" #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" #include "esphome/components/binary_sensor/filter.h" @@ -34,52 +33,39 @@ namespace binary_sensor { * The sub classes should notify the front-end of new states via the publish_state() method which * handles inverted inputs for you. */ -class BinarySensor : public EntityBase, public EntityBase_DeviceClass { +class BinarySensor : public StatefulEntityBase, public EntityBase_DeviceClass { public: - explicit BinarySensor(); - - /** Add a callback to be notified of state changes. - * - * @param callback The void(bool) callback. - */ - void add_on_state_callback(std::function &&callback); + explicit BinarySensor(){}; /** Publish a new state to the front-end. * - * @param state The new state. + * @param new_state The new state. */ - void publish_state(bool state); + void publish_state(bool new_state); /** Publish the initial state, this will not make the callback manager send callbacks * and is meant only for the initial state on boot. * - * @param state The new state. + * @param new_state The new state. */ - void publish_initial_state(bool state); - - /// The current reported state of the binary sensor. - bool state{false}; + void publish_initial_state(bool new_state); void add_filter(Filter *filter); void add_filters(const std::vector &filters); - void set_publish_initial_state(bool publish_initial_state) { this->publish_initial_state_ = publish_initial_state; } - // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) - void send_state_internal(bool state, bool is_initial); + void send_state_internal(bool new_state); /// Return whether this binary sensor has outputted a state. - virtual bool has_state() const; - virtual bool is_status_binary_sensor() const; + // For backward compatibility, provide an accessible property + + bool state{}; + protected: - CallbackManager state_callback_{}; Filter *filter_list_{nullptr}; - bool has_state_{false}; - bool publish_initial_state_{false}; - Deduplicator publish_dedup_; }; class BinarySensorInitiallyOff : public BinarySensor { diff --git a/esphome/components/binary_sensor/filter.cpp b/esphome/components/binary_sensor/filter.cpp index 8f94b108ac..3567e9c72b 100644 --- a/esphome/components/binary_sensor/filter.cpp +++ b/esphome/components/binary_sensor/filter.cpp @@ -9,37 +9,42 @@ namespace binary_sensor { static const char *const TAG = "sensor.filter"; -void Filter::output(bool value, bool is_initial) { +void Filter::output(bool value) { + if (this->next_ == nullptr) { + this->parent_->send_state_internal(value); + } else { + this->next_->input(value); + } +} +void Filter::input(bool value) { if (!this->dedup_.next(value)) return; - - if (this->next_ == nullptr) { - this->parent_->send_state_internal(value, is_initial); - } else { - this->next_->input(value, is_initial); - } -} -void Filter::input(bool value, bool is_initial) { - auto b = this->new_value(value, is_initial); + auto b = this->new_value(value); if (b.has_value()) { - this->output(*b, is_initial); + this->output(*b); } } -optional DelayedOnOffFilter::new_value(bool value, bool is_initial) { +void TimeoutFilter::input(bool value) { + this->set_timeout("timeout", this->timeout_delay_.value(), [this]() { this->parent_->invalidate_state(); }); + // we do not de-dup here otherwise changes from invalid to valid state will not be output + this->output(value); +} + +optional DelayedOnOffFilter::new_value(bool value) { if (value) { - this->set_timeout("ON_OFF", this->on_delay_.value(), [this, is_initial]() { this->output(true, is_initial); }); + this->set_timeout("ON_OFF", this->on_delay_.value(), [this]() { this->output(true); }); } else { - this->set_timeout("ON_OFF", this->off_delay_.value(), [this, is_initial]() { this->output(false, is_initial); }); + this->set_timeout("ON_OFF", this->off_delay_.value(), [this]() { this->output(false); }); } return {}; } float DelayedOnOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; } -optional DelayedOnFilter::new_value(bool value, bool is_initial) { +optional DelayedOnFilter::new_value(bool value) { if (value) { - this->set_timeout("ON", this->delay_.value(), [this, is_initial]() { this->output(true, is_initial); }); + this->set_timeout("ON", this->delay_.value(), [this]() { this->output(true); }); return {}; } else { this->cancel_timeout("ON"); @@ -49,9 +54,9 @@ optional DelayedOnFilter::new_value(bool value, bool is_initial) { float DelayedOnFilter::get_setup_priority() const { return setup_priority::HARDWARE; } -optional DelayedOffFilter::new_value(bool value, bool is_initial) { +optional DelayedOffFilter::new_value(bool value) { if (!value) { - this->set_timeout("OFF", this->delay_.value(), [this, is_initial]() { this->output(false, is_initial); }); + this->set_timeout("OFF", this->delay_.value(), [this]() { this->output(false); }); return {}; } else { this->cancel_timeout("OFF"); @@ -61,11 +66,11 @@ optional DelayedOffFilter::new_value(bool value, bool is_initial) { float DelayedOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; } -optional InvertFilter::new_value(bool value, bool is_initial) { return !value; } +optional InvertFilter::new_value(bool value) { return !value; } AutorepeatFilter::AutorepeatFilter(std::vector timings) : timings_(std::move(timings)) {} -optional AutorepeatFilter::new_value(bool value, bool is_initial) { +optional AutorepeatFilter::new_value(bool value) { if (value) { // Ignore if already running if (this->active_timing_ != 0) @@ -101,7 +106,7 @@ void AutorepeatFilter::next_timing_() { void AutorepeatFilter::next_value_(bool val) { const AutorepeatFilterTiming &timing = this->timings_[this->active_timing_ - 2]; - this->output(val, false); // This is at least the second one so not initial + this->output(val); // This is at least the second one so not initial this->set_timeout("ON_OFF", val ? timing.time_on : timing.time_off, [this, val]() { this->next_value_(!val); }); } @@ -109,18 +114,18 @@ float AutorepeatFilter::get_setup_priority() const { return setup_priority::HARD LambdaFilter::LambdaFilter(std::function(bool)> f) : f_(std::move(f)) {} -optional LambdaFilter::new_value(bool value, bool is_initial) { return this->f_(value); } +optional LambdaFilter::new_value(bool value) { return this->f_(value); } -optional SettleFilter::new_value(bool value, bool is_initial) { +optional SettleFilter::new_value(bool value) { if (!this->steady_) { - this->set_timeout("SETTLE", this->delay_.value(), [this, value, is_initial]() { + this->set_timeout("SETTLE", this->delay_.value(), [this, value]() { this->steady_ = true; - this->output(value, is_initial); + this->output(value); }); return {}; } else { this->steady_ = false; - this->output(value, is_initial); + this->output(value); this->set_timeout("SETTLE", this->delay_.value(), [this]() { this->steady_ = true; }); return value; } diff --git a/esphome/components/binary_sensor/filter.h b/esphome/components/binary_sensor/filter.h index f7342db2fb..16f44aa5fe 100644 --- a/esphome/components/binary_sensor/filter.h +++ b/esphome/components/binary_sensor/filter.h @@ -14,11 +14,11 @@ class BinarySensor; class Filter { public: - virtual optional new_value(bool value, bool is_initial) = 0; + virtual optional new_value(bool value) = 0; - void input(bool value, bool is_initial); + virtual void input(bool value); - void output(bool value, bool is_initial); + void output(bool value); protected: friend BinarySensor; @@ -28,9 +28,19 @@ class Filter { Deduplicator dedup_; }; +class TimeoutFilter : public Filter, public Component { + public: + optional new_value(bool value) override { return value; } + void input(bool value) override; + template void set_timeout_value(T timeout) { this->timeout_delay_ = timeout; } + + protected: + TemplatableValue timeout_delay_{}; +}; + class DelayedOnOffFilter : public Filter, public Component { public: - optional new_value(bool value, bool is_initial) override; + optional new_value(bool value) override; float get_setup_priority() const override; @@ -44,7 +54,7 @@ class DelayedOnOffFilter : public Filter, public Component { class DelayedOnFilter : public Filter, public Component { public: - optional new_value(bool value, bool is_initial) override; + optional new_value(bool value) override; float get_setup_priority() const override; @@ -56,7 +66,7 @@ class DelayedOnFilter : public Filter, public Component { class DelayedOffFilter : public Filter, public Component { public: - optional new_value(bool value, bool is_initial) override; + optional new_value(bool value) override; float get_setup_priority() const override; @@ -68,7 +78,7 @@ class DelayedOffFilter : public Filter, public Component { class InvertFilter : public Filter { public: - optional new_value(bool value, bool is_initial) override; + optional new_value(bool value) override; }; struct AutorepeatFilterTiming { @@ -86,7 +96,7 @@ class AutorepeatFilter : public Filter, public Component { public: explicit AutorepeatFilter(std::vector timings); - optional new_value(bool value, bool is_initial) override; + optional new_value(bool value) override; float get_setup_priority() const override; @@ -102,7 +112,7 @@ class LambdaFilter : public Filter { public: explicit LambdaFilter(std::function(bool)> f); - optional new_value(bool value, bool is_initial) override; + optional new_value(bool value) override; protected: std::function(bool)> f_; @@ -110,7 +120,7 @@ class LambdaFilter : public Filter { class SettleFilter : public Filter, public Component { public: - optional new_value(bool value, bool is_initial) override; + optional new_value(bool value) override; float get_setup_priority() const override; diff --git a/esphome/components/ble_client/output/ble_binary_output.h b/esphome/components/ble_client/output/ble_binary_output.h index 0a1e186b26..5e8bd6da62 100644 --- a/esphome/components/ble_client/output/ble_binary_output.h +++ b/esphome/components/ble_client/output/ble_binary_output.h @@ -16,7 +16,6 @@ class BLEBinaryOutput : public output::BinaryOutput, public BLEClientNode, publi public: void dump_config() override; void loop() override {} - float get_setup_priority() const override { return setup_priority::DATA; } void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } diff --git a/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp b/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp index 81d244ce6d..663c52ac10 100644 --- a/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp +++ b/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp @@ -11,7 +11,11 @@ namespace ble_client { static const char *const TAG = "ble_rssi_sensor"; -void BLEClientRSSISensor::loop() {} +void BLEClientRSSISensor::loop() { + // Parent BLEClientNode has a loop() method, but this component uses + // polling via update() and BLE GAP callbacks so loop isn't needed + this->disable_loop(); +} void BLEClientRSSISensor::dump_config() { LOG_SENSOR("", "BLE Client RSSI Sensor", this); diff --git a/esphome/components/ble_client/sensor/ble_rssi_sensor.h b/esphome/components/ble_client/sensor/ble_rssi_sensor.h index 5dd3fc7af9..76cd8345a6 100644 --- a/esphome/components/ble_client/sensor/ble_rssi_sensor.h +++ b/esphome/components/ble_client/sensor/ble_rssi_sensor.h @@ -18,7 +18,6 @@ class BLEClientRSSISensor : public sensor::Sensor, public PollingComponent, publ void loop() override; void update() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; diff --git a/esphome/components/ble_client/sensor/ble_sensor.cpp b/esphome/components/ble_client/sensor/ble_sensor.cpp index f91b07fee2..d0ccfe1f2e 100644 --- a/esphome/components/ble_client/sensor/ble_sensor.cpp +++ b/esphome/components/ble_client/sensor/ble_sensor.cpp @@ -11,7 +11,11 @@ namespace ble_client { static const char *const TAG = "ble_sensor"; -void BLESensor::loop() {} +void BLESensor::loop() { + // Parent BLEClientNode has a loop() method, but this component uses + // polling via update() and BLE callbacks so loop isn't needed + this->disable_loop(); +} void BLESensor::dump_config() { LOG_SENSOR("", "BLE Sensor", this); diff --git a/esphome/components/ble_client/sensor/ble_sensor.h b/esphome/components/ble_client/sensor/ble_sensor.h index b11a010ee4..24d1ed2fd2 100644 --- a/esphome/components/ble_client/sensor/ble_sensor.h +++ b/esphome/components/ble_client/sensor/ble_sensor.h @@ -24,7 +24,6 @@ class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClie void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } diff --git a/esphome/components/ble_client/switch/ble_switch.h b/esphome/components/ble_client/switch/ble_switch.h index 2e19c8aeef..9809f904e7 100644 --- a/esphome/components/ble_client/switch/ble_switch.h +++ b/esphome/components/ble_client/switch/ble_switch.h @@ -19,7 +19,6 @@ class BLEClientSwitch : public switch_::Switch, public Component, public BLEClie void loop() override {} void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: void write_state(bool state) override; diff --git a/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp b/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp index 5083e235c6..e7da297fa0 100644 --- a/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp +++ b/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp @@ -14,7 +14,11 @@ static const char *const TAG = "ble_text_sensor"; static const std::string EMPTY = ""; -void BLETextSensor::loop() {} +void BLETextSensor::loop() { + // Parent BLEClientNode has a loop() method, but this component uses + // polling via update() and BLE callbacks so loop isn't needed + this->disable_loop(); +} void BLETextSensor::dump_config() { LOG_TEXT_SENSOR("", "BLE Text Sensor", this); diff --git a/esphome/components/ble_client/text_sensor/ble_text_sensor.h b/esphome/components/ble_client/text_sensor/ble_text_sensor.h index cb34043b46..c75a4df952 100644 --- a/esphome/components/ble_client/text_sensor/ble_text_sensor.h +++ b/esphome/components/ble_client/text_sensor/ble_text_sensor.h @@ -20,7 +20,6 @@ class BLETextSensor : public text_sensor::TextSensor, public PollingComponent, p void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } diff --git a/esphome/components/ble_presence/ble_presence_device.h b/esphome/components/ble_presence/ble_presence_device.h index 3ed60d1b49..70ecc67c32 100644 --- a/esphome/components/ble_presence/ble_presence_device.h +++ b/esphome/components/ble_presence/ble_presence_device.h @@ -105,7 +105,6 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, this->set_found_(false); } void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: void set_found_(bool state) { diff --git a/esphome/components/ble_rssi/ble_rssi_sensor.h b/esphome/components/ble_rssi/ble_rssi_sensor.h index 89e4f33aca..80245a1fe1 100644 --- a/esphome/components/ble_rssi/ble_rssi_sensor.h +++ b/esphome/components/ble_rssi/ble_rssi_sensor.h @@ -99,7 +99,6 @@ class BLERSSISensor : public sensor::Sensor, public esp32_ble_tracker::ESPBTDevi return false; } void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: enum MatchType { MATCH_BY_MAC_ADDRESS, MATCH_BY_IRK, MATCH_BY_SERVICE_UUID, MATCH_BY_IBEACON_UUID }; diff --git a/esphome/components/ble_scanner/ble_scanner.h b/esphome/components/ble_scanner/ble_scanner.h index b330eff696..8bb51fcff2 100644 --- a/esphome/components/ble_scanner/ble_scanner.h +++ b/esphome/components/ble_scanner/ble_scanner.h @@ -29,7 +29,6 @@ class BLEScanner : public text_sensor::TextSensor, public esp32_ble_tracker::ESP return true; } void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } }; } // namespace ble_scanner diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.h b/esphome/components/bluetooth_proxy/bluetooth_connection.h index fd83f8dd00..73c034d93b 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.h +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.h @@ -26,10 +26,17 @@ class BluetoothConnection : public esp32_ble_client::BLEClientBase { protected: friend class BluetoothProxy; - bool seen_mtu_or_services_{false}; - int16_t send_service_{-2}; + // Memory optimized layout for 32-bit systems + // Group 1: Pointers (4 bytes each, naturally aligned) BluetoothProxy *proxy_; + + // Group 2: 2-byte types + int16_t send_service_{-2}; // Needs to handle negative values and service count + + // Group 3: 1-byte types + bool seen_mtu_or_services_{false}; + // 1 byte used, 1 byte padding }; } // namespace bluetooth_proxy diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index fbe2a3e67c..a5e8ec0860 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -52,11 +52,21 @@ bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device) return true; } -static constexpr size_t FLUSH_BATCH_SIZE = 8; -static std::vector &get_batch_buffer() { - static std::vector batch_buffer; - return batch_buffer; -} +// Batch size for BLE advertisements to maximize WiFi efficiency +// Each advertisement is up to 80 bytes when packaged (including protocol overhead) +// Most advertisements are 20-30 bytes, allowing even more to fit per packet +// 16 advertisements × 80 bytes (worst case) = 1280 bytes out of ~1320 bytes usable payload +// This achieves ~97% WiFi MTU utilization while staying under the limit +static constexpr size_t FLUSH_BATCH_SIZE = 16; + +namespace { +// Batch buffer in anonymous namespace to avoid guard variable (saves 8 bytes) +// This is initialized at program startup before any threads +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +std::vector batch_buffer; +} // namespace + +static std::vector &get_batch_buffer() { return batch_buffer; } bool BluetoothProxy::parse_devices(const esp32_ble::BLEScanResult *scan_results, size_t count) { if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr || !this->raw_advertisements_) @@ -170,7 +180,7 @@ int BluetoothProxy::get_bluetooth_connections_free() { void BluetoothProxy::loop() { if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr) { for (auto *connection : this->connections_) { - if (connection->get_address() != 0) { + if (connection->get_address() != 0 && !connection->disconnect_pending()) { connection->disconnect(); } } diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index 16db0a0a11..f0632350e0 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -134,11 +134,17 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com BluetoothConnection *get_connection_(uint64_t address, bool reserve); - bool active_; - - std::vector connections_{}; + // Memory optimized layout for 32-bit systems + // Group 1: Pointers (4 bytes each, naturally aligned) api::APIConnection *api_connection_{nullptr}; + + // Group 2: Container types (typically 12 bytes on 32-bit) + std::vector connections_{}; + + // Group 3: 1-byte types grouped together + bool active_; bool raw_advertisements_{false}; + // 2 bytes used, 2 bytes padding }; extern BluetoothProxy *global_bluetooth_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/bme680/sensor.py b/esphome/components/bme680/sensor.py index abdf6d3969..f41aefcec3 100644 --- a/esphome/components/bme680/sensor.py +++ b/esphome/components/bme680/sensor.py @@ -12,8 +12,8 @@ from esphome.const import ( CONF_OVERSAMPLING, CONF_PRESSURE, CONF_TEMPERATURE, - DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ATMOSPHERIC_PRESSURE, + DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, ICON_GAS_CYLINDER, STATE_CLASS_MEASUREMENT, diff --git a/esphome/components/bmp581/bmp581.h b/esphome/components/bmp581/bmp581.h index 7327be44ae..1d7e932fa1 100644 --- a/esphome/components/bmp581/bmp581.h +++ b/esphome/components/bmp581/bmp581.h @@ -61,8 +61,6 @@ enum IIRFilter { class BMP581Component : public PollingComponent, public i2c::I2CDevice { public: - float get_setup_priority() const override { return setup_priority::DATA; } - void dump_config() override; void setup() override; diff --git a/esphome/components/button/__init__.py b/esphome/components/button/__init__.py index 892bf62f3a..ed2670a5c5 100644 --- a/esphome/components/button/__init__.py +++ b/esphome/components/button/__init__.py @@ -18,8 +18,8 @@ from esphome.const import ( DEVICE_CLASS_UPDATE, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@esphome/core"] IS_PLATFORM_COMPONENT = True @@ -61,6 +61,9 @@ _BUTTON_SCHEMA = ( ) +_BUTTON_SCHEMA.add_extra(entity_duplicate_validator("button")) + + def button_schema( class_: MockObjClass, *, @@ -87,7 +90,7 @@ BUTTON_SCHEMA.add_extra(cv.deprecated_schema_constant("button")) async def setup_button_core_(var, config): - await setup_entity(var, config) + await setup_entity(var, config, "button") for conf in config.get(CONF_ON_PRESS, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) diff --git a/esphome/components/camera/__init__.py b/esphome/components/camera/__init__.py new file mode 100644 index 0000000000..a19f7707af --- /dev/null +++ b/esphome/components/camera/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@DT-art1", "@bdraco"] diff --git a/esphome/components/camera/camera.cpp b/esphome/components/camera/camera.cpp new file mode 100644 index 0000000000..3bd632af5c --- /dev/null +++ b/esphome/components/camera/camera.cpp @@ -0,0 +1,22 @@ +#include "camera.h" + +namespace esphome { +namespace camera { + +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +Camera *Camera::global_camera = nullptr; + +Camera::Camera() { + if (global_camera != nullptr) { + this->status_set_error("Multiple cameras are configured, but only one is supported."); + this->mark_failed(); + return; + } + + global_camera = this; +} + +Camera *Camera::instance() { return global_camera; } + +} // namespace camera +} // namespace esphome diff --git a/esphome/components/camera/camera.h b/esphome/components/camera/camera.h new file mode 100644 index 0000000000..fb9da58cc1 --- /dev/null +++ b/esphome/components/camera/camera.h @@ -0,0 +1,80 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/entity_base.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace camera { + +/** Different sources for filtering. + * IDLE: Camera requests to send an image to the API. + * API_REQUESTER: API requests a new image. + * WEB_REQUESTER: ESP32 web server request an image. Ignored by API. + */ +enum CameraRequester : uint8_t { IDLE, API_REQUESTER, WEB_REQUESTER }; + +/** Abstract camera image base class. + * Encapsulates the JPEG encoded data and it is shared among + * all connected clients. + */ +class CameraImage { + public: + virtual uint8_t *get_data_buffer() = 0; + virtual size_t get_data_length() = 0; + virtual bool was_requested_by(CameraRequester requester) const = 0; + virtual ~CameraImage() {} +}; + +/** Abstract image reader base class. + * Keeps track of the data offset of the camera image and + * how many bytes are remaining to read. When the image + * is returned, the shared_ptr is reset and the camera can + * reuse the memory of the camera image. + */ +class CameraImageReader { + public: + virtual void set_image(std::shared_ptr image) = 0; + virtual size_t available() const = 0; + virtual uint8_t *peek_data_buffer() = 0; + virtual void consume_data(size_t consumed) = 0; + virtual void return_image() = 0; + virtual ~CameraImageReader() {} +}; + +/** Abstract camera base class. Collaborates with API. + * 1) API server starts and installs callback (add_image_callback) + * which is called by the camera when a new image is available. + * 2) New API client connects and creates a new image reader (create_image_reader). + * 3) API connection receives protobuf CameraImageRequest and calls request_image. + * 3.a) API connection receives protobuf CameraImageRequest and calls start_stream. + * 4) Camera implementation provides JPEG data in the CameraImage and calls callback. + * 5) API connection sets the image in the image reader. + * 6) API connection consumes data from the image reader and returns the image when finished. + * 7.a) Camera captures a new image and continues with 4) until start_stream is called. + */ +class Camera : public EntityBase, public Component { + public: + Camera(); + // Camera implementation invokes callback to publish a new image. + virtual void add_image_callback(std::function)> &&callback) = 0; + /// Returns a new camera image reader that keeps track of the JPEG data in the camera image. + virtual CameraImageReader *create_image_reader() = 0; + // Connection, camera or web server requests one new JPEG image. + virtual void request_image(CameraRequester requester) = 0; + // Connection, camera or web server requests a stream of images. + virtual void start_stream(CameraRequester requester) = 0; + // Connection or web server stops the previously started stream. + virtual void stop_stream(CameraRequester requester) = 0; + virtual ~Camera() {} + /// The singleton instance of the camera implementation. + static Camera *instance(); + + protected: + // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) + static Camera *global_camera; +}; + +} // namespace camera +} // namespace esphome diff --git a/esphome/components/canbus/__init__.py b/esphome/components/canbus/__init__.py index 6867177795..cdb57fd481 100644 --- a/esphome/components/canbus/__init__.py +++ b/esphome/components/canbus/__init__.py @@ -1,4 +1,5 @@ import re + from esphome import automation import esphome.codegen as cg import esphome.config_validation as cv diff --git a/esphome/components/cap1188/cap1188.h b/esphome/components/cap1188/cap1188.h index fa0ed622fa..baefd1c48f 100644 --- a/esphome/components/cap1188/cap1188.h +++ b/esphome/components/cap1188/cap1188.h @@ -46,7 +46,6 @@ class CAP1188Component : public Component, public i2c::I2CDevice { void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void loop() override; protected: diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index ea11e733ac..7e8afd8fab 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -7,11 +7,12 @@ from esphome.const import ( PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, + PLATFORM_LN882X, PLATFORM_RTL87XX, ) from esphome.core import CORE, coroutine_with_priority -AUTO_LOAD = ["web_server_base"] +AUTO_LOAD = ["web_server_base", "ota.web_server"] DEPENDENCIES = ["wifi"] CODEOWNERS = ["@OttoWinter"] @@ -27,7 +28,15 @@ CONFIG_SCHEMA = cv.All( ), } ).extend(cv.COMPONENT_SCHEMA), - cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX, PLATFORM_RTL87XX]), + cv.only_on( + [ + PLATFORM_ESP32, + PLATFORM_ESP8266, + PLATFORM_BK72XX, + PLATFORM_LN882X, + PLATFORM_RTL87XX, + ] + ), ) @@ -41,6 +50,7 @@ async def to_code(config): if CORE.using_arduino: if CORE.is_esp32: + cg.add_library("ESP32 Async UDP", None) cg.add_library("DNSServer", None) cg.add_library("WiFi", None) if CORE.is_esp8266: diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index 31e6c51f0f..25179fdacc 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -37,12 +37,16 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { request->redirect("/?save"); } -void CaptivePortal::setup() {} +void CaptivePortal::setup() { +#ifndef USE_ARDUINO + // No DNS server needed for non-Arduino frameworks + this->disable_loop(); +#endif +} void CaptivePortal::start() { this->base_->init(); if (!this->initialized_) { this->base_->add_handler(this); - this->base_->add_ota_handler(); } #ifdef USE_ARDUINO @@ -50,6 +54,8 @@ void CaptivePortal::start() { this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError); network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip(); this->dns_server_->start(53, "*", ip); + // Re-enable loop() when DNS server is started + this->enable_loop(); #endif this->base_->get_server()->onNotFound([this](AsyncWebServerRequest *req) { @@ -68,7 +74,11 @@ void CaptivePortal::start() { void CaptivePortal::handleRequest(AsyncWebServerRequest *req) { if (req->url() == "/") { +#ifndef USE_ESP8266 + auto *response = req->beginResponse(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ)); +#else auto *response = req->beginResponse_P(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ)); +#endif response->addHeader("Content-Encoding", "gzip"); req->send(response); return; diff --git a/esphome/components/captive_portal/captive_portal.h b/esphome/components/captive_portal/captive_portal.h index 24d1295e6a..c78fff824a 100644 --- a/esphome/components/captive_portal/captive_portal.h +++ b/esphome/components/captive_portal/captive_portal.h @@ -21,8 +21,11 @@ class CaptivePortal : public AsyncWebHandler, public Component { void dump_config() override; #ifdef USE_ARDUINO void loop() override { - if (this->dns_server_ != nullptr) + if (this->dns_server_ != nullptr) { this->dns_server_->processNextRequest(); + } else { + this->disable_loop(); + } } #endif float get_setup_priority() const override; @@ -37,7 +40,7 @@ class CaptivePortal : public AsyncWebHandler, public Component { #endif } - bool canHandle(AsyncWebServerRequest *request) override { + bool canHandle(AsyncWebServerRequest *request) const override { if (!this->active_) return false; diff --git a/esphome/components/ccs811/ccs811.h b/esphome/components/ccs811/ccs811.h index 8a0d60d002..675ba7da97 100644 --- a/esphome/components/ccs811/ccs811.h +++ b/esphome/components/ccs811/ccs811.h @@ -25,8 +25,6 @@ class CCS811Component : public PollingComponent, public i2c::I2CDevice { void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } - protected: optional read_status_() { return this->read_byte(0x00); } bool status_has_error_() { return this->read_status_().value_or(1) & 1; } diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index 52938a17d0..9530ecdcca 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -48,8 +48,8 @@ from esphome.const import ( CONF_WEB_SERVER, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity IS_PLATFORM_COMPONENT = True @@ -247,6 +247,9 @@ _CLIMATE_SCHEMA = ( ) +_CLIMATE_SCHEMA.add_extra(entity_duplicate_validator("climate")) + + def climate_schema( class_: MockObjClass, *, @@ -273,7 +276,7 @@ CLIMATE_SCHEMA.add_extra(cv.deprecated_schema_constant("climate")) async def setup_climate_core_(var, config): - await setup_entity(var, config) + await setup_entity(var, config, "climate") visual = config[CONF_VISUAL] if (min_temp := visual.get(CONF_MIN_TEMPERATURE)) is not None: diff --git a/esphome/components/cm1106/sensor.py b/esphome/components/cm1106/sensor.py index 1b8ac14fbe..1d95bcc666 100644 --- a/esphome/components/cm1106/sensor.py +++ b/esphome/components/cm1106/sensor.py @@ -1,10 +1,10 @@ """CM1106 Sensor component for ESPHome.""" -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation from esphome.automation import maybe_simple_id +import esphome.codegen as cg from esphome.components import sensor, uart +import esphome.config_validation as cv from esphome.const import ( CONF_CO2, CONF_ID, diff --git a/esphome/components/const/__init__.py b/esphome/components/const/__init__.py index a73849e67d..b084622f4c 100644 --- a/esphome/components/const/__init__.py +++ b/esphome/components/const/__init__.py @@ -2,5 +2,7 @@ CODEOWNERS = ["@esphome/core"] +CONF_BYTE_ORDER = "byte_order" CONF_DRAW_ROUNDING = "draw_rounding" +CONF_ON_STATE_CHANGE = "on_state_change" CONF_REQUEST_HEADERS = "request_headers" diff --git a/esphome/components/copy/binary_sensor/copy_binary_sensor.h b/esphome/components/copy/binary_sensor/copy_binary_sensor.h index d62ed13c76..fc1e368b38 100644 --- a/esphome/components/copy/binary_sensor/copy_binary_sensor.h +++ b/esphome/components/copy/binary_sensor/copy_binary_sensor.h @@ -11,7 +11,6 @@ class CopyBinarySensor : public binary_sensor::BinarySensor, public Component { void set_source(binary_sensor::BinarySensor *source) { source_ = source; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: binary_sensor::BinarySensor *source_; diff --git a/esphome/components/copy/button/copy_button.h b/esphome/components/copy/button/copy_button.h index 9996ca0c65..79d5dbcf04 100644 --- a/esphome/components/copy/button/copy_button.h +++ b/esphome/components/copy/button/copy_button.h @@ -10,7 +10,6 @@ class CopyButton : public button::Button, public Component { public: void set_source(button::Button *source) { source_ = source; } void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: void press_action() override; diff --git a/esphome/components/copy/cover/copy_cover.h b/esphome/components/copy/cover/copy_cover.h index fb278523ff..ec27b6782a 100644 --- a/esphome/components/copy/cover/copy_cover.h +++ b/esphome/components/copy/cover/copy_cover.h @@ -11,7 +11,6 @@ class CopyCover : public cover::Cover, public Component { void set_source(cover::Cover *source) { source_ = source; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } cover::CoverTraits get_traits() override; diff --git a/esphome/components/copy/fan/copy_fan.h b/esphome/components/copy/fan/copy_fan.h index 1a69810510..b474975bc4 100644 --- a/esphome/components/copy/fan/copy_fan.h +++ b/esphome/components/copy/fan/copy_fan.h @@ -11,7 +11,6 @@ class CopyFan : public fan::Fan, public Component { void set_source(fan::Fan *source) { source_ = source; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } fan::FanTraits get_traits() override; diff --git a/esphome/components/copy/lock/copy_lock.h b/esphome/components/copy/lock/copy_lock.h index 0554013674..8799eebb4a 100644 --- a/esphome/components/copy/lock/copy_lock.h +++ b/esphome/components/copy/lock/copy_lock.h @@ -11,7 +11,6 @@ class CopyLock : public lock::Lock, public Component { void set_source(lock::Lock *source) { source_ = source; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: void control(const lock::LockCall &call) override; diff --git a/esphome/components/copy/number/copy_number.h b/esphome/components/copy/number/copy_number.h index 1ad956fec4..09b65e2cbf 100644 --- a/esphome/components/copy/number/copy_number.h +++ b/esphome/components/copy/number/copy_number.h @@ -11,7 +11,6 @@ class CopyNumber : public number::Number, public Component { void set_source(number::Number *source) { source_ = source; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: void control(float value) override; diff --git a/esphome/components/copy/select/copy_select.h b/esphome/components/copy/select/copy_select.h index c8666cd394..fb0aee86f6 100644 --- a/esphome/components/copy/select/copy_select.h +++ b/esphome/components/copy/select/copy_select.h @@ -11,7 +11,6 @@ class CopySelect : public select::Select, public Component { void set_source(select::Select *source) { source_ = source; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: void control(const std::string &value) override; diff --git a/esphome/components/copy/sensor/copy_sensor.h b/esphome/components/copy/sensor/copy_sensor.h index 1ae790ada3..500e6872fe 100644 --- a/esphome/components/copy/sensor/copy_sensor.h +++ b/esphome/components/copy/sensor/copy_sensor.h @@ -11,7 +11,6 @@ class CopySensor : public sensor::Sensor, public Component { void set_source(sensor::Sensor *source) { source_ = source; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: sensor::Sensor *source_; diff --git a/esphome/components/copy/switch/copy_switch.h b/esphome/components/copy/switch/copy_switch.h index 26cb254ab3..80310af03f 100644 --- a/esphome/components/copy/switch/copy_switch.h +++ b/esphome/components/copy/switch/copy_switch.h @@ -11,7 +11,6 @@ class CopySwitch : public switch_::Switch, public Component { void set_source(switch_::Switch *source) { source_ = source; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: void write_state(bool state) override; diff --git a/esphome/components/copy/text/copy_text.h b/esphome/components/copy/text/copy_text.h index beb8610dfe..9eaebae4be 100644 --- a/esphome/components/copy/text/copy_text.h +++ b/esphome/components/copy/text/copy_text.h @@ -11,7 +11,6 @@ class CopyText : public text::Text, public Component { void set_source(text::Text *source) { source_ = source; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: void control(const std::string &value) override; diff --git a/esphome/components/copy/text_sensor/copy_text_sensor.h b/esphome/components/copy/text_sensor/copy_text_sensor.h index fe91fe948b..489986c59d 100644 --- a/esphome/components/copy/text_sensor/copy_text_sensor.h +++ b/esphome/components/copy/text_sensor/copy_text_sensor.h @@ -11,7 +11,6 @@ class CopyTextSensor : public text_sensor::TextSensor, public Component { void set_source(text_sensor::TextSensor *source) { source_ = source; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: text_sensor::TextSensor *source_; diff --git a/esphome/components/cover/__init__.py b/esphome/components/cover/__init__.py index 9fe7593eab..cd97a38ecc 100644 --- a/esphome/components/cover/__init__.py +++ b/esphome/components/cover/__init__.py @@ -33,8 +33,8 @@ from esphome.const import ( DEVICE_CLASS_WINDOW, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity IS_PLATFORM_COMPONENT = True @@ -126,6 +126,9 @@ _COVER_SCHEMA = ( ) +_COVER_SCHEMA.add_extra(entity_duplicate_validator("cover")) + + def cover_schema( class_: MockObjClass, *, @@ -154,7 +157,7 @@ COVER_SCHEMA.add_extra(cv.deprecated_schema_constant("cover")) async def setup_cover_core_(var, config): - await setup_entity(var, config) + await setup_entity(var, config, "cover") if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: cg.add(var.set_device_class(device_class)) diff --git a/esphome/components/cs5460a/cs5460a.h b/esphome/components/cs5460a/cs5460a.h index 763ddc14fa..15ae04f3c6 100644 --- a/esphome/components/cs5460a/cs5460a.h +++ b/esphome/components/cs5460a/cs5460a.h @@ -77,7 +77,6 @@ class CS5460AComponent : public Component, void setup() override; void loop() override {} - float get_setup_priority() const override { return setup_priority::DATA; } void dump_config() override; protected: diff --git a/esphome/components/datetime/__init__.py b/esphome/components/datetime/__init__.py index 24fbf5a1ec..4788810965 100644 --- a/esphome/components/datetime/__init__.py +++ b/esphome/components/datetime/__init__.py @@ -22,8 +22,8 @@ from esphome.const import ( CONF_YEAR, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@rfdarter", "@jesserockz"] @@ -84,6 +84,8 @@ _DATETIME_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) ).add_extra(_validate_time_present) +_DATETIME_SCHEMA.add_extra(entity_duplicate_validator("datetime")) + def date_schema(class_: MockObjClass) -> cv.Schema: schema = cv.Schema( @@ -133,7 +135,7 @@ def datetime_schema(class_: MockObjClass) -> cv.Schema: async def setup_datetime_core_(var, config): - await setup_entity(var, config) + await setup_entity(var, config, "datetime") if (mqtt_id := config.get(CONF_MQTT_ID)) is not None: mqtt_ = cg.new_Pvariable(mqtt_id, var) diff --git a/esphome/components/debug/__init__.py b/esphome/components/debug/__init__.py index 1955b5d22c..500dfac1fe 100644 --- a/esphome/components/debug/__init__.py +++ b/esphome/components/debug/__init__.py @@ -1,4 +1,5 @@ import esphome.codegen as cg +from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( CONF_BLOCK, @@ -7,6 +8,7 @@ from esphome.const import ( CONF_FREE, CONF_ID, CONF_LOOP_TIME, + PlatformFramework, ) CODEOWNERS = ["@OttoWinter"] @@ -44,3 +46,21 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) + + +FILTER_SOURCE_FILES = filter_source_files_from_platform( + { + "debug_esp32.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + }, + "debug_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, + "debug_host.cpp": {PlatformFramework.HOST_NATIVE}, + "debug_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO}, + "debug_libretiny.cpp": { + PlatformFramework.BK72XX_ARDUINO, + PlatformFramework.RTL87XX_ARDUINO, + PlatformFramework.LN882X_ARDUINO, + }, + } +) diff --git a/esphome/components/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py index 63b359bd5b..05ae60239d 100644 --- a/esphome/components/deep_sleep/__init__.py +++ b/esphome/components/deep_sleep/__init__.py @@ -1,6 +1,6 @@ from esphome import automation, pins 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.const import ( VARIANT_ESP32, @@ -11,6 +11,7 @@ from esphome.components.esp32.const import ( VARIANT_ESP32S2, VARIANT_ESP32S3, ) +from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( CONF_DEFAULT, @@ -27,6 +28,7 @@ from esphome.const import ( CONF_WAKEUP_PIN, PLATFORM_ESP32, PLATFORM_ESP8266, + PlatformFramework, ) WAKEUP_PINS = { @@ -114,12 +116,20 @@ def validate_pin_number(value): return value -def validate_config(config): - if get_esp32_variant() == VARIANT_ESP32C3 and CONF_ESP32_EXT1_WAKEUP in config: - raise cv.Invalid("ESP32-C3 does not support wakeup from touch.") - if get_esp32_variant() == VARIANT_ESP32C3 and CONF_TOUCH_WAKEUP in config: - raise cv.Invalid("ESP32-C3 does not support wakeup from ext1") - return config +def _validate_ex1_wakeup_mode(value): + if value == "ALL_LOW": + esp32.only_on_variant(supported=[VARIANT_ESP32], msg_prefix="ALL_LOW")(value) + if value == "ANY_LOW": + esp32.only_on_variant( + 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") @@ -146,6 +156,7 @@ WAKEUP_PIN_MODES = { esp_sleep_ext1_wakeup_mode_t = cg.global_ns.enum("esp_sleep_ext1_wakeup_mode_t") Ext1Wakeup = deep_sleep_ns.struct("Ext1Wakeup") 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, "ANY_HIGH": esp_sleep_ext1_wakeup_mode_t.ESP_EXT1_WAKEUP_ANY_HIGH, } @@ -185,16 +196,28 @@ CONFIG_SCHEMA = cv.All( ), cv.Optional(CONF_ESP32_EXT1_WAKEUP): cv.All( cv.only_on_esp32, + esp32.only_on_variant( + unsupported=[VARIANT_ESP32C3], msg_prefix="Wakeup from ext1" + ), cv.Schema( { cv.Required(CONF_PINS): cv.ensure_list( 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), cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266]), @@ -313,3 +336,14 @@ async def deep_sleep_action_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var + + +FILTER_SOURCE_FILES = filter_source_files_from_platform( + { + "deep_sleep_esp32.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + }, + "deep_sleep_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, + } +) diff --git a/esphome/components/demo/__init__.py b/esphome/components/demo/__init__.py index 0a56073284..2af0c18c18 100644 --- a/esphome/components/demo/__init__.py +++ b/esphome/components/demo/__init__.py @@ -455,7 +455,7 @@ CONFIG_SCHEMA = cv.Schema( CONF_NAME: "Demo Plain Sensor", }, { - CONF_NAME: "Demo Temperature Sensor", + CONF_NAME: "Demo Temperature Sensor 1", CONF_UNIT_OF_MEASUREMENT: UNIT_CELSIUS, CONF_ICON: ICON_THERMOMETER, CONF_ACCURACY_DECIMALS: 1, @@ -463,7 +463,7 @@ CONFIG_SCHEMA = cv.Schema( CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, { - CONF_NAME: "Demo Temperature Sensor", + CONF_NAME: "Demo Temperature Sensor 2", CONF_UNIT_OF_MEASUREMENT: UNIT_CELSIUS, CONF_ICON: ICON_THERMOMETER, CONF_ACCURACY_DECIMALS: 1, diff --git a/esphome/components/display/display.cpp b/esphome/components/display/display.cpp index a13464ce1b..c666eee298 100644 --- a/esphome/components/display/display.cpp +++ b/esphome/components/display/display.cpp @@ -1,5 +1,6 @@ #include "display.h" #include +#include #include "display_color_utils.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" @@ -424,15 +425,15 @@ void HOT Display::get_regular_polygon_vertex(int vertex_id, int *vertex_x, int * // hence we rotate the shape by 270° to orient the polygon up. rotation_degrees += ROTATION_270_DEGREES; // Convert the rotation to radians, easier to use in trigonometrical calculations - float rotation_radians = rotation_degrees * PI / 180; + float rotation_radians = rotation_degrees * std::numbers::pi / 180; // A pointy top variation means the first vertex of the polygon is at the top center of the shape, this requires no // additional rotation of the shape. // A flat top variation means the first point of the polygon has to be rotated so that the first edge is horizontal, // this requires to rotate the shape by π/edges radians counter-clockwise so that the first point is located on the // left side of the first horizontal edge. - rotation_radians -= (variation == VARIATION_FLAT_TOP) ? PI / edges : 0.0; + rotation_radians -= (variation == VARIATION_FLAT_TOP) ? std::numbers::pi / edges : 0.0; - float vertex_angle = ((float) vertex_id) / edges * 2 * PI + rotation_radians; + float vertex_angle = ((float) vertex_id) / edges * 2 * std::numbers::pi + rotation_radians; *vertex_x = (int) round(cos(vertex_angle) * radius) + center_x; *vertex_y = (int) round(sin(vertex_angle) * radius) + center_y; } diff --git a/esphome/components/display/display.h b/esphome/components/display/display.h index 68c1184721..f2d79d12d9 100644 --- a/esphome/components/display/display.h +++ b/esphome/components/display/display.h @@ -138,8 +138,6 @@ enum DisplayRotation { DISPLAY_ROTATION_270_DEGREES = 270, }; -#define PI 3.1415926535897932384626433832795 - const int EDGES_TRIGON = 3; const int EDGES_TRIANGLE = 3; const int EDGES_TETRAGON = 4; diff --git a/esphome/components/display/display_buffer.cpp b/esphome/components/display/display_buffer.cpp index 3af1b63e01..0ecdccc38a 100644 --- a/esphome/components/display/display_buffer.cpp +++ b/esphome/components/display/display_buffer.cpp @@ -11,7 +11,7 @@ namespace display { static const char *const TAG = "display"; void DisplayBuffer::init_internal_(uint32_t buffer_length) { - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator allocator; this->buffer_ = allocator.allocate(buffer_length); if (this->buffer_ == nullptr) { ESP_LOGE(TAG, "Could not allocate buffer for display!"); diff --git a/esphome/components/ds2484/__init__.py b/esphome/components/ds2484/__init__.py new file mode 100644 index 0000000000..3d9f24ff19 --- /dev/null +++ b/esphome/components/ds2484/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@mrk-its"] diff --git a/esphome/components/ds2484/ds2484.cpp b/esphome/components/ds2484/ds2484.cpp new file mode 100644 index 0000000000..c3df9786b6 --- /dev/null +++ b/esphome/components/ds2484/ds2484.cpp @@ -0,0 +1,209 @@ +#include "ds2484.h" + +namespace esphome { +namespace ds2484 { +static const char *const TAG = "ds2484.onewire"; + +void DS2484OneWireBus::setup() { + ESP_LOGCONFIG(TAG, "Running setup"); + this->reset_device(); + this->search(); +} + +void DS2484OneWireBus::dump_config() { + ESP_LOGCONFIG(TAG, "1-wire bus:"); + this->dump_devices_(TAG); +} + +bool DS2484OneWireBus::read_status_(uint8_t *status) { + for (uint8_t retry_nr = 0; retry_nr < 10; retry_nr++) { + if (this->read(status, 1) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "read status error"); + return false; + } + ESP_LOGVV(TAG, "status: %02x", *status); + if (!(*status & 1)) { + return true; + } + } + ESP_LOGE(TAG, "read status error: too many retries"); + return false; +} + +bool DS2484OneWireBus::wait_for_completion_() { + uint8_t status; + return this->read_status_(&status); +} + +bool DS2484OneWireBus::reset_device() { + ESP_LOGVV(TAG, "reset_device"); + uint8_t device_reset_cmd = 0xf0; + uint8_t response; + if (this->write(&device_reset_cmd, 1) != i2c::ERROR_OK) { + return false; + } + if (!this->wait_for_completion_()) { + ESP_LOGE(TAG, "reset_device: can't complete"); + return false; + } + uint8_t config = (this->active_pullup_ ? 1 : 0) | (this->strong_pullup_ ? 4 : 0); + uint8_t write_config[2] = {0xd2, (uint8_t) (config | (~config << 4))}; + if (this->write(write_config, 2) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "reset_device: can't write config"); + return false; + } + if (this->read(&response, 1) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "can't read read8 response"); + return false; + } + if (response != (write_config[1] & 0xf)) { + ESP_LOGE(TAG, "configuration didn't update"); + return false; + } + return true; +}; + +int DS2484OneWireBus::reset_int() { + ESP_LOGVV(TAG, "reset"); + uint8_t reset_cmd = 0xb4; + if (this->write(&reset_cmd, 1) != i2c::ERROR_OK) { + return -1; + } + return this->wait_for_completion_() ? 1 : 0; +}; + +void DS2484OneWireBus::write8_(uint8_t value) { + uint8_t buffer[2] = {0xa5, value}; + this->write(buffer, 2); + this->wait_for_completion_(); +}; + +void DS2484OneWireBus::write8(uint8_t value) { + ESP_LOGVV(TAG, "write8: %02x", value); + this->write8_(value); +}; + +void DS2484OneWireBus::write64(uint64_t value) { + ESP_LOGVV(TAG, "write64: %llx", value); + for (uint8_t i = 0; i < 8; i++) { + this->write8_((value >> (i * 8)) & 0xff); + } +} + +uint8_t DS2484OneWireBus::read8() { + uint8_t read8_cmd = 0x96; + uint8_t set_read_reg_cmd[2] = {0xe1, 0xe1}; + uint8_t response = 0; + if (this->write(&read8_cmd, 1) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "can't write read8 cmd"); + return 0; + } + this->wait_for_completion_(); + if (this->write(set_read_reg_cmd, 2) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "can't set read data reg"); + return 0; + } + if (this->read(&response, 1) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "can't read read8 response"); + return 0; + } + return response; +} + +uint64_t DS2484OneWireBus::read64() { + uint8_t response = 0; + for (uint8_t i = 0; i < 8; i++) { + response |= (this->read8() << (i * 8)); + } + return response; +} + +void DS2484OneWireBus::reset_search() { + this->last_discrepancy_ = 0; + this->last_device_flag_ = false; + this->address_ = 0; +} + +bool DS2484OneWireBus::one_wire_triple_(bool *branch, bool *id_bit, bool *cmp_id_bit) { + uint8_t buffer[2] = {(uint8_t) 0x78, (uint8_t) (*branch ? 0x80u : 0)}; + uint8_t status; + if (!this->read_status_(&status)) { + ESP_LOGE(TAG, "one_wire_triple start: read status error"); + return false; + } + if (this->write(buffer, 2) != i2c::ERROR_OK) { + ESP_LOGV(TAG, "one_wire_triple: can't write cmd"); + return false; + } + if (!this->read_status_(&status)) { + ESP_LOGE(TAG, "one_wire_triple: read status error"); + return false; + } + *id_bit = bool(status & 0x20); + *cmp_id_bit = bool(status & 0x40); + *branch = bool(status & 0x80); + return true; +} + +uint64_t IRAM_ATTR DS2484OneWireBus::search_int() { + ESP_LOGVV(TAG, "search_int"); + if (this->last_device_flag_) { + ESP_LOGVV(TAG, "last device flag set, quitting"); + return 0u; + } + + uint8_t last_zero = 0; + uint64_t bit_mask = 1; + uint64_t address = this->address_; + + // Initiate search + for (uint8_t bit_number = 1; bit_number <= 64; bit_number++, bit_mask <<= 1) { + bool branch; + + // compute branch value for the case when there is a discrepancy + // (there are devices with both 0s and 1s at this bit) + if (bit_number < this->last_discrepancy_) { + branch = (address & bit_mask) > 0; + } else { + branch = bit_number == this->last_discrepancy_; + } + + bool id_bit, cmp_id_bit; + bool branch_before = branch; + if (!this->one_wire_triple_(&branch, &id_bit, &cmp_id_bit)) { + ESP_LOGW(TAG, "one wire triple error, quitting"); + return 0; + } + + if (id_bit && cmp_id_bit) { + ESP_LOGW(TAG, "no devices on the bus, quitting"); + // No devices participating in search + return 0; + } + + if (!id_bit && !cmp_id_bit && !branch) { + last_zero = bit_number; + } + + ESP_LOGVV(TAG, "%d %d branch: %d %d", id_bit, cmp_id_bit, branch_before, branch); + + if (branch) { + address |= bit_mask; + } else { + address &= ~bit_mask; + } + } + ESP_LOGVV(TAG, "last_discepancy: %d", last_zero); + ESP_LOGVV(TAG, "address: %llx", address); + this->last_discrepancy_ = last_zero; + if (this->last_discrepancy_ == 0) { + // we're at root and have no choices left, so this was the last one. + this->last_device_flag_ = true; + } + + this->address_ = address; + return address; +} + +} // namespace ds2484 +} // namespace esphome diff --git a/esphome/components/ds2484/ds2484.h b/esphome/components/ds2484/ds2484.h new file mode 100644 index 0000000000..223227c0a2 --- /dev/null +++ b/esphome/components/ds2484/ds2484.h @@ -0,0 +1,43 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/core/preferences.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/one_wire/one_wire.h" + +namespace esphome { +namespace ds2484 { + +class DS2484OneWireBus : public one_wire::OneWireBus, public i2c::I2CDevice, public Component { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::BUS - 1.0; } + + bool reset_device(); + int reset_int() override; + void write8(uint8_t) override; + void write64(uint64_t) override; + uint8_t read8() override; + uint64_t read64() override; + + void set_active_pullup(bool value) { this->active_pullup_ = value; } + void set_strong_pullup(bool value) { this->strong_pullup_ = value; } + + protected: + void reset_search() override; + uint64_t search_int() override; + bool read_status_(uint8_t *); + bool wait_for_completion_(); + void write8_(uint8_t); + bool one_wire_triple_(bool *branch, bool *id_bit, bool *cmp_id_bit); + + uint64_t address_; + uint8_t last_discrepancy_{0}; + bool last_device_flag_{false}; + bool active_pullup_{false}; + bool strong_pullup_{false}; +}; +} // namespace ds2484 +} // namespace esphome diff --git a/esphome/components/ds2484/one_wire.py b/esphome/components/ds2484/one_wire.py new file mode 100644 index 0000000000..384b2d01e6 --- /dev/null +++ b/esphome/components/ds2484/one_wire.py @@ -0,0 +1,37 @@ +import esphome.codegen as cg +from esphome.components import i2c +from esphome.components.one_wire import OneWireBus +import esphome.config_validation as cv +from esphome.const import CONF_ID + +ds2484_ns = cg.esphome_ns.namespace("ds2484") + +CONF_ACTIVE_PULLUP = "active_pullup" +CONF_STRONG_PULLUP = "strong_pullup" + +CODEOWNERS = ["@mrk-its"] +DEPENDENCIES = ["i2c"] + +DS2484OneWireBus = ds2484_ns.class_( + "DS2484OneWireBus", OneWireBus, i2c.I2CDevice, cg.Component +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(DS2484OneWireBus), + cv.Optional(CONF_ACTIVE_PULLUP, default=False): cv.boolean, + cv.Optional(CONF_STRONG_PULLUP, default=False): cv.boolean, + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x18)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await i2c.register_i2c_device(var, config) + await cg.register_component(var, config) + cg.add(var.set_active_pullup(config[CONF_ACTIVE_PULLUP])) + cg.add(var.set_strong_pullup(config[CONF_STRONG_PULLUP])) diff --git a/esphome/components/duty_time/duty_time_sensor.h b/esphome/components/duty_time/duty_time_sensor.h index 38655f104a..18280f8e21 100644 --- a/esphome/components/duty_time/duty_time_sensor.h +++ b/esphome/components/duty_time/duty_time_sensor.h @@ -19,7 +19,6 @@ class DutyTimeSensor : public sensor::Sensor, public PollingComponent { void update() override; void loop() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void start(); void stop(); diff --git a/esphome/components/ens160_base/ens160_base.h b/esphome/components/ens160_base/ens160_base.h index 729225a5ae..ae850c8180 100644 --- a/esphome/components/ens160_base/ens160_base.h +++ b/esphome/components/ens160_base/ens160_base.h @@ -18,7 +18,6 @@ class ENS160Component : public PollingComponent, public sensor::Sensor { void setup() override; void update() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: void send_env_data_(); diff --git a/esphome/components/es7210/es7210.h b/esphome/components/es7210/es7210.h index 8f6d9d8136..7071a547ec 100644 --- a/esphome/components/es7210/es7210.h +++ b/esphome/components/es7210/es7210.h @@ -25,7 +25,6 @@ class ES7210 : public audio_adc::AudioAdc, public Component, public i2c::I2CDevi */ public: void setup() override; - float get_setup_priority() const override { return setup_priority::DATA; } void dump_config() override; void set_bits_per_sample(ES7210BitsPerSample bits_per_sample) { this->bits_per_sample_ = bits_per_sample; } diff --git a/esphome/components/es7243e/es7243e.h b/esphome/components/es7243e/es7243e.h index 41a8acac8d..f7c9d67371 100644 --- a/esphome/components/es7243e/es7243e.h +++ b/esphome/components/es7243e/es7243e.h @@ -14,7 +14,6 @@ class ES7243E : public audio_adc::AudioAdc, public Component, public i2c::I2CDev */ public: void setup() override; - float get_setup_priority() const override { return setup_priority::DATA; } void dump_config() override; bool set_mic_gain(float mic_gain) override; diff --git a/esphome/components/es8156/es8156.h b/esphome/components/es8156/es8156.h index e973599a7a..082514485c 100644 --- a/esphome/components/es8156/es8156.h +++ b/esphome/components/es8156/es8156.h @@ -14,7 +14,6 @@ class ES8156 : public audio_dac::AudioDac, public Component, public i2c::I2CDevi ///////////////////////// void setup() override; - float get_setup_priority() const override { return setup_priority::DATA; } void dump_config() override; //////////////////////// diff --git a/esphome/components/es8311/es8311.h b/esphome/components/es8311/es8311.h index 840a07204c..5eccc48004 100644 --- a/esphome/components/es8311/es8311.h +++ b/esphome/components/es8311/es8311.h @@ -50,7 +50,6 @@ class ES8311 : public audio_dac::AudioDac, public Component, public i2c::I2CDevi ///////////////////////// void setup() override; - float get_setup_priority() const override { return setup_priority::DATA; } void dump_config() override; //////////////////////// diff --git a/esphome/components/es8388/es8388.h b/esphome/components/es8388/es8388.h index 45944f68bd..373f71b437 100644 --- a/esphome/components/es8388/es8388.h +++ b/esphome/components/es8388/es8388.h @@ -38,7 +38,6 @@ class ES8388 : public audio_dac::AudioDac, public Component, public i2c::I2CDevi ///////////////////////// void setup() override; - float get_setup_priority() const override { return setup_priority::DATA; } void dump_config() override; //////////////////////// diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index ba9a4894c2..8408f902ef 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -4,7 +4,7 @@ import logging import os from pathlib import Path -from esphome import git +from esphome import yaml_util import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import ( @@ -23,7 +23,6 @@ from esphome.const import ( CONF_REFRESH, CONF_SOURCE, CONF_TYPE, - CONF_URL, CONF_VARIANT, CONF_VERSION, KEY_CORE, @@ -32,14 +31,13 @@ from esphome.const import ( KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM, PLATFORM_ESP32, - TYPE_GIT, - TYPE_LOCAL, __version__, ) from esphome.core import CORE, HexInt, TimePeriod from esphome.cpp_generator import RawExpression import esphome.final_validate as fv from esphome.helpers import copy_file_if_changed, mkdir_p, write_file_if_changed +from esphome.types import ConfigType from .boards import BOARDS from .const import ( # noqa @@ -49,10 +47,8 @@ from .const import ( # noqa KEY_EXTRA_BUILD_FILES, KEY_PATH, KEY_REF, - KEY_REFRESH, KEY_REPO, KEY_SDKCONFIG_OPTIONS, - KEY_SUBMODULES, KEY_VARIANT, VARIANT_ESP32, VARIANT_ESP32C2, @@ -132,6 +128,8 @@ def set_core_data(config): choices = CPU_FREQUENCIES[variant] if "160MHZ" in choices: cpu_frequency = "160MHZ" + elif "360MHZ" in choices: + cpu_frequency = "360MHZ" else: cpu_frequency = choices[-1] config[CONF_CPU_FREQUENCY] = cpu_frequency @@ -191,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.""" if supported is not None and not isinstance(supported, list): supported = [supported] @@ -202,11 +200,11 @@ def only_on_variant(*, supported=None, unsupported=None): variant = get_esp32_variant() if supported is not None and variant not in supported: 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: raise cv.Invalid( - f"This feature is not available on {', '.join(unsupported)}" + f"{msg_prefix} is not available on {', '.join(unsupported)}" ) return obj @@ -233,7 +231,7 @@ def add_idf_sdkconfig_option(name: str, value: SdkconfigValueType): def add_idf_component( *, name: str, - repo: str, + repo: str = None, ref: str = None, path: str = None, refresh: TimePeriod = None, @@ -243,30 +241,27 @@ def add_idf_component( """Add an esp-idf component to the project.""" if not CORE.using_esp_idf: raise ValueError("Not an esp-idf project") - if components is None: - components = [] - if name not in CORE.data[KEY_ESP32][KEY_COMPONENTS]: + if not repo and not ref and not path: + raise ValueError("Requires at least one of repo, ref or path") + if refresh or submodules or components: + _LOGGER.warning( + "The refresh, components and submodules parameters in add_idf_component() are " + "deprecated and will be removed in ESPHome 2026.1. If you are seeing this, report " + "an issue to the external_component author and ask them to update it." + ) + if components: + for comp in components: + CORE.data[KEY_ESP32][KEY_COMPONENTS][comp] = { + KEY_REPO: repo, + KEY_REF: ref, + KEY_PATH: f"{path}/{comp}" if path else comp, + } + else: CORE.data[KEY_ESP32][KEY_COMPONENTS][name] = { KEY_REPO: repo, KEY_REF: ref, KEY_PATH: path, - KEY_REFRESH: refresh, - KEY_COMPONENTS: components, - KEY_SUBMODULES: submodules, } - else: - component_config = CORE.data[KEY_ESP32][KEY_COMPONENTS][name] - if components is not None: - component_config[KEY_COMPONENTS] = list( - set(component_config[KEY_COMPONENTS] + components) - ) - if submodules is not None: - if component_config[KEY_SUBMODULES] is None: - component_config[KEY_SUBMODULES] = submodules - else: - component_config[KEY_SUBMODULES] = list( - set(component_config[KEY_SUBMODULES] + submodules) - ) def add_extra_script(stage: str, filename: str, path: str): @@ -289,11 +284,8 @@ def add_extra_build_file(filename: str, path: str) -> bool: def _format_framework_arduino_version(ver: cv.Version) -> str: # format the given arduino (https://github.com/espressif/arduino-esp32/releases) version to - # a PIO platformio/framework-arduinoespressif32 value - # List of package versions: https://api.registry.platformio.org/v3/packages/platformio/tool/framework-arduinoespressif32 - if ver <= cv.Version(1, 0, 3): - return f"~2.{ver.major}{ver.minor:02d}{ver.patch:02d}.0" - return f"~3.{ver.major}{ver.minor:02d}{ver.patch:02d}.0" + # a PIO pioarduino/framework-arduinoespressif32 value + return f"pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/{str(ver)}/esp32-{str(ver)}.zip" def _format_framework_espidf_version( @@ -317,12 +309,10 @@ def _format_framework_espidf_version( # The default/recommended arduino framework version # - https://github.com/espressif/arduino-esp32/releases -# - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-arduinoespressif32 -RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(2, 0, 5) -# The platformio/espressif32 version to use for arduino frameworks -# - https://github.com/platformio/platform-espressif32/releases -# - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32 -ARDUINO_PLATFORM_VERSION = cv.Version(5, 4, 0) +RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 1, 3) +# The platform-espressif32 version to use for arduino frameworks +# - https://github.com/pioarduino/platform-espressif32/releases +ARDUINO_PLATFORM_VERSION = cv.Version(53, 3, 13) # The default/recommended esp-idf framework version # - https://github.com/espressif/esp-idf/releases @@ -351,6 +341,7 @@ SUPPORTED_PLATFORMIO_ESP_IDF_5X = [ # List based on https://github.com/pioarduino/esp-idf/releases SUPPORTED_PIOARDUINO_ESP_IDF_5X = [ cv.Version(5, 5, 0), + cv.Version(5, 4, 2), cv.Version(5, 4, 1), cv.Version(5, 4, 0), cv.Version(5, 3, 3), @@ -365,8 +356,8 @@ SUPPORTED_PIOARDUINO_ESP_IDF_5X = [ def _arduino_check_versions(value): value = value.copy() lookups = { - "dev": (cv.Version(2, 1, 0), "https://github.com/espressif/arduino-esp32.git"), - "latest": (cv.Version(2, 0, 9), None), + "dev": (cv.Version(3, 1, 3), "https://github.com/espressif/arduino-esp32.git"), + "latest": (cv.Version(3, 1, 3), None), "recommended": (RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, None), } @@ -388,6 +379,10 @@ def _arduino_check_versions(value): CONF_PLATFORM_VERSION, _parse_platform_version(str(ARDUINO_PLATFORM_VERSION)) ) + if value[CONF_SOURCE].startswith("http"): + # prefix is necessary or platformio will complain with a cryptic error + value[CONF_SOURCE] = f"framework-arduinoespressif32@{value[CONF_SOURCE]}" + if version != RECOMMENDED_ARDUINO_FRAMEWORK_VERSION: _LOGGER.warning( "The selected Arduino framework version is not the recommended one. " @@ -416,8 +411,8 @@ def _esp_idf_check_versions(value): version = cv.Version.parse(cv.version_number(value[CONF_VERSION])) source = value.get(CONF_SOURCE, None) - if version < cv.Version(4, 0, 0): - raise cv.Invalid("Only ESP-IDF 4.0+ is supported.") + if version < cv.Version(5, 0, 0): + raise cv.Invalid("Only ESP-IDF 5.0+ is supported.") # flag this for later *before* we set value[CONF_PLATFORM_VERSION] below has_platform_ver = CONF_PLATFORM_VERSION in value @@ -427,20 +422,15 @@ def _esp_idf_check_versions(value): ) if ( - (is_platformio := _platform_is_platformio(value[CONF_PLATFORM_VERSION])) - and version.major >= 5 - and version not in SUPPORTED_PLATFORMIO_ESP_IDF_5X - ): + is_platformio := _platform_is_platformio(value[CONF_PLATFORM_VERSION]) + ) and version not in SUPPORTED_PLATFORMIO_ESP_IDF_5X: raise cv.Invalid( f"ESP-IDF {str(version)} not supported by platformio/espressif32" ) if ( - version.major < 5 - or ( - version in SUPPORTED_PLATFORMIO_ESP_IDF_5X - and version not in SUPPORTED_PIOARDUINO_ESP_IDF_5X - ) + version in SUPPORTED_PLATFORMIO_ESP_IDF_5X + and version not in SUPPORTED_PIOARDUINO_ESP_IDF_5X ) and not has_platform_ver: raise cv.Invalid( f"ESP-IDF {value[CONF_VERSION]} may be supported by platformio/espressif32; please specify '{CONF_PLATFORM_VERSION}'" @@ -574,6 +564,17 @@ CONF_ENABLE_LWIP_DHCP_SERVER = "enable_lwip_dhcp_server" CONF_ENABLE_LWIP_MDNS_QUERIES = "enable_lwip_mdns_queries" CONF_ENABLE_LWIP_BRIDGE_INTERFACE = "enable_lwip_bridge_interface" + +def _validate_idf_component(config: ConfigType) -> ConfigType: + """Validate IDF component config and warn about deprecated options.""" + if CONF_REFRESH in config: + _LOGGER.warning( + "The 'refresh' option for IDF components is deprecated and has no effect. " + "It will be removed in ESPHome 2026.1. Please remove it from your configuration." + ) + return config + + ESP_IDF_FRAMEWORK_SCHEMA = cv.All( cv.Schema( { @@ -613,15 +614,19 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All( } ), cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list( - cv.Schema( - { - cv.Required(CONF_NAME): cv.string_strict, - cv.Required(CONF_SOURCE): cv.SOURCE_SCHEMA, - cv.Optional(CONF_PATH): cv.string, - cv.Optional(CONF_REFRESH, default="1d"): cv.All( - cv.string, cv.source_refresh - ), - } + cv.All( + cv.Schema( + { + cv.Required(CONF_NAME): cv.string_strict, + cv.Optional(CONF_SOURCE): cv.git_ref, + cv.Optional(CONF_REF): cv.string, + cv.Optional(CONF_PATH): cv.string, + cv.Optional(CONF_REFRESH): cv.All( + cv.string, cv.source_refresh + ), + } + ), + _validate_idf_component, ) ), } @@ -695,6 +700,7 @@ FINAL_VALIDATE_SCHEMA = cv.Schema(final_validate) async def to_code(config): cg.add_platformio_option("board", config[CONF_BOARD]) cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE]) + cg.set_cpp_standard("gnu++20") cg.add_build_flag("-DUSE_ESP32") cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{config[CONF_VARIANT]}") @@ -748,6 +754,9 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0", False) add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1", False) + # Disable dynamic log level control to save memory + add_idf_sdkconfig_option("CONFIG_LOG_DYNAMIC_LEVEL_CONTROL", False) + # Set default CPU frequency add_idf_sdkconfig_option(f"CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_{freq}", True) @@ -787,14 +796,9 @@ async def to_code(config): if advanced.get(CONF_IGNORE_EFUSE_MAC_CRC): add_idf_sdkconfig_option("CONFIG_ESP_MAC_IGNORE_MAC_CRC_ERROR", True) - if (framework_ver.major, framework_ver.minor) >= (4, 4): - add_idf_sdkconfig_option( - "CONFIG_ESP_PHY_CALIBRATION_AND_DATA_STORAGE", False - ) - else: - add_idf_sdkconfig_option( - "CONFIG_ESP32_PHY_CALIBRATION_AND_DATA_STORAGE", False - ) + add_idf_sdkconfig_option( + "CONFIG_ESP_PHY_CALIBRATION_AND_DATA_STORAGE", False + ) if advanced.get(CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES): _LOGGER.warning( "Using experimental features in ESP-IDF may result in unexpected failures." @@ -812,26 +816,17 @@ async def to_code(config): add_idf_sdkconfig_option(name, RawSdkconfigValue(value)) for component in conf[CONF_COMPONENTS]: - source = component[CONF_SOURCE] - if source[CONF_TYPE] == TYPE_GIT: - add_idf_component( - name=component[CONF_NAME], - repo=source[CONF_URL], - ref=source.get(CONF_REF), - path=component.get(CONF_PATH), - refresh=component[CONF_REFRESH], - ) - elif source[CONF_TYPE] == TYPE_LOCAL: - _LOGGER.warning("Local components are not implemented yet.") - + add_idf_component( + name=component[CONF_NAME], + repo=component.get(CONF_SOURCE), + ref=component.get(CONF_REF), + path=component.get(CONF_PATH), + ) elif conf[CONF_TYPE] == FRAMEWORK_ARDUINO: cg.add_platformio_option("framework", "arduino") cg.add_build_flag("-DUSE_ARDUINO") cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ARDUINO") - cg.add_platformio_option( - "platform_packages", - [f"platformio/framework-arduinoespressif32@{conf[CONF_SOURCE]}"], - ) + cg.add_platformio_option("platform_packages", [conf[CONF_SOURCE]]) if CONF_PARTITIONS in config: cg.add_platformio_option("board_build.partitions", config[CONF_PARTITIONS]) @@ -925,6 +920,26 @@ def _write_sdkconfig(): write_file_if_changed(sdk_path, contents) +def _write_idf_component_yml(): + yml_path = Path(CORE.relative_build_path("src/idf_component.yml")) + if CORE.data[KEY_ESP32][KEY_COMPONENTS]: + components: dict = CORE.data[KEY_ESP32][KEY_COMPONENTS] + dependencies = {} + for name, component in components.items(): + dependency = {} + if component[KEY_REF]: + dependency["version"] = component[KEY_REF] + if component[KEY_REPO]: + dependency["git"] = component[KEY_REPO] + if component[KEY_PATH]: + dependency["path"] = component[KEY_PATH] + dependencies[name] = dependency + contents = yaml_util.dump({"dependencies": dependencies}) + else: + contents = "" + write_file_if_changed(yml_path, contents) + + # Called by writer.py def copy_files(): if CORE.using_arduino: @@ -937,6 +952,7 @@ def copy_files(): ) if CORE.using_esp_idf: _write_sdkconfig() + _write_idf_component_yml() if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]: write_file_if_changed( CORE.relative_build_path("partitions.csv"), @@ -953,55 +969,6 @@ def copy_files(): __version__, ) - import shutil - - shutil.rmtree(CORE.relative_build_path("components"), ignore_errors=True) - - if CORE.data[KEY_ESP32][KEY_COMPONENTS]: - components: dict = CORE.data[KEY_ESP32][KEY_COMPONENTS] - - for name, component in components.items(): - repo_dir, _ = git.clone_or_update( - url=component[KEY_REPO], - ref=component[KEY_REF], - refresh=component[KEY_REFRESH], - domain="idf_components", - submodules=component[KEY_SUBMODULES], - ) - mkdir_p(CORE.relative_build_path("components")) - component_dir = repo_dir - if component[KEY_PATH] is not None: - component_dir = component_dir / component[KEY_PATH] - - if component[KEY_COMPONENTS] == ["*"]: - shutil.copytree( - component_dir, - CORE.relative_build_path("components"), - dirs_exist_ok=True, - ignore=shutil.ignore_patterns(".git*"), - symlinks=True, - ignore_dangling_symlinks=True, - ) - elif len(component[KEY_COMPONENTS]) > 0: - for comp in component[KEY_COMPONENTS]: - shutil.copytree( - component_dir / comp, - CORE.relative_build_path(f"components/{comp}"), - dirs_exist_ok=True, - ignore=shutil.ignore_patterns(".git*"), - symlinks=True, - ignore_dangling_symlinks=True, - ) - else: - shutil.copytree( - component_dir, - CORE.relative_build_path(f"components/{name}"), - dirs_exist_ok=True, - ignore=shutil.ignore_patterns(".git*"), - symlinks=True, - ignore_dangling_symlinks=True, - ) - for _, file in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES].items(): if file[KEY_PATH].startswith("http"): import requests diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index 562bcba3c2..f3bdfea2a0 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -56,11 +56,7 @@ void arch_init() { void IRAM_ATTR HOT arch_feed_wdt() { esp_task_wdt_reset(); } uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } -#if ESP_IDF_VERSION_MAJOR >= 5 uint32_t arch_get_cpu_cycle_count() { return esp_cpu_get_cycle_count(); } -#else -uint32_t arch_get_cpu_cycle_count() { return cpu_hal_get_cycle_count(); } -#endif uint32_t arch_get_cpu_freq_hz() { uint32_t freq = 0; #ifdef USE_ESP_IDF diff --git a/esphome/components/esp32/gpio.h b/esphome/components/esp32/gpio.h index d69ac1c493..0fefc1c058 100644 --- a/esphome/components/esp32/gpio.h +++ b/esphome/components/esp32/gpio.h @@ -29,9 +29,9 @@ class ESP32InternalGPIOPin : public InternalGPIOPin { void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; gpio_num_t pin_; - bool inverted_; gpio_drive_cap_t drive_strength_; gpio::Flags flags_; + bool inverted_; // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) static bool isr_service_installed; }; diff --git a/esphome/components/esp32/helpers.cpp b/esphome/components/esp32/helpers.cpp new file mode 100644 index 0000000000..310e7bd94a --- /dev/null +++ b/esphome/components/esp32/helpers.cpp @@ -0,0 +1,69 @@ +#include "esphome/core/helpers.h" + +#ifdef USE_ESP32 + +#include "esp_efuse.h" +#include "esp_efuse_table.h" +#include "esp_mac.h" + +#include +#include +#include "esp_random.h" +#include "esp_system.h" + +namespace esphome { + +uint32_t random_uint32() { return esp_random(); } +bool random_bytes(uint8_t *data, size_t len) { + esp_fill_random(data, len); + return true; +} + +Mutex::Mutex() { handle_ = xSemaphoreCreateMutex(); } +Mutex::~Mutex() {} +void Mutex::lock() { xSemaphoreTake(this->handle_, portMAX_DELAY); } +bool Mutex::try_lock() { return xSemaphoreTake(this->handle_, 0) == pdTRUE; } +void Mutex::unlock() { xSemaphoreGive(this->handle_); } + +// only affects the executing core +// so should not be used as a mutex lock, only to get accurate timing +IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); } +IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); } + +void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter) +#if defined(CONFIG_SOC_IEEE802154_SUPPORTED) + // When CONFIG_SOC_IEEE802154_SUPPORTED is defined, esp_efuse_mac_get_default + // returns the 802.15.4 EUI-64 address, so we read directly from eFuse instead. + if (has_custom_mac_address()) { + esp_efuse_read_field_blob(ESP_EFUSE_MAC_CUSTOM, mac, 48); + } else { + esp_efuse_read_field_blob(ESP_EFUSE_MAC_FACTORY, mac, 48); + } +#else + if (has_custom_mac_address()) { + esp_efuse_mac_get_custom(mac); + } else { + esp_efuse_mac_get_default(mac); + } +#endif +} + +void set_mac_address(uint8_t *mac) { esp_base_mac_addr_set(mac); } + +bool has_custom_mac_address() { +#if !defined(USE_ESP32_IGNORE_EFUSE_CUSTOM_MAC) + uint8_t mac[6]; + // do not use 'esp_efuse_mac_get_custom(mac)' because it drops an error in the logs whenever it fails +#ifndef USE_ESP32_VARIANT_ESP32 + return (esp_efuse_read_field_blob(ESP_EFUSE_USER_DATA_MAC_CUSTOM, mac, 48) == ESP_OK) && mac_address_is_valid(mac); +#else + return (esp_efuse_read_field_blob(ESP_EFUSE_MAC_CUSTOM, mac, 48) == ESP_OK) && mac_address_is_valid(mac); +#endif +#else + return false; +#endif +} + +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index cf63ad34d7..8b0cf4da98 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -1,9 +1,9 @@ #ifdef USE_ESP32 #include "ble.h" -#include "ble_event_pool.h" #include "esphome/core/application.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #include @@ -516,13 +516,12 @@ void ESP32BLE::dump_config() { break; } ESP_LOGCONFIG(TAG, - "ESP32 BLE:\n" - " MAC address: %02X:%02X:%02X:%02X:%02X:%02X\n" + "BLE:\n" + " MAC address: %s\n" " IO Capability: %s", - mac_address[0], mac_address[1], mac_address[2], mac_address[3], mac_address[4], mac_address[5], - io_capability_s); + format_mac_address_pretty(mac_address).c_str(), io_capability_s); } else { - ESP_LOGCONFIG(TAG, "ESP32 BLE: bluetooth stack is not enabled"); + ESP_LOGCONFIG(TAG, "Bluetooth stack is not enabled"); } } diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 9fe996086e..2c5697df82 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -12,8 +12,8 @@ #include "esphome/core/helpers.h" #include "ble_event.h" -#include "ble_event_pool.h" -#include "queue.h" +#include "esphome/core/lock_free_queue.h" +#include "esphome/core/event_pool.h" #ifdef USE_ESP32 @@ -25,10 +25,15 @@ namespace esphome { namespace esp32_ble { // Maximum number of BLE scan results to buffer +// Sized to handle bursts of advertisements while allowing for processing delays +// With 16 advertisements per batch and some safety margin: +// - Without PSRAM: 24 entries (1.5× batch size) +// - With PSRAM: 36 entries (2.25× batch size) +// The reduced structure size (~80 bytes vs ~400 bytes) allows for larger buffers #ifdef USE_PSRAM -static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 32; +static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 36; #else -static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 20; +static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 24; #endif // Maximum size of the BLE event queue - must be power of 2 for lock-free queue @@ -51,7 +56,7 @@ enum IoCapability { IO_CAP_KBDISP = ESP_IO_CAP_KBDISP, }; -enum BLEComponentState { +enum BLEComponentState : uint8_t { /** Nothing has been initialized yet. */ BLE_COMPONENT_STATE_OFF = 0, /** BLE should be disabled on next loop. */ @@ -141,21 +146,31 @@ class ESP32BLE : public Component { private: template friend void enqueue_ble_event(Args... args); + // Vectors (12 bytes each on 32-bit, naturally aligned to 4 bytes) std::vector gap_event_handlers_; std::vector gap_scan_event_handlers_; std::vector gattc_event_handlers_; std::vector gatts_event_handlers_; std::vector ble_status_event_handlers_; - BLEComponentState state_{BLE_COMPONENT_STATE_OFF}; - LockFreeQueue ble_events_; - BLEEventPool ble_event_pool_; - BLEAdvertising *advertising_{}; - esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; - uint32_t advertising_cycle_time_{}; - bool enable_on_boot_{}; + // Large objects (size depends on template parameters, but typically aligned to 4 bytes) + esphome::LockFreeQueue ble_events_; + esphome::EventPool ble_event_pool_; + + // optional (typically 16+ bytes on 32-bit, aligned to 4 bytes) optional name_; - uint16_t appearance_{0}; + + // 4-byte aligned members + BLEAdvertising *advertising_{}; // 4 bytes (pointer) + esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; // 4 bytes (enum) + uint32_t advertising_cycle_time_{}; // 4 bytes + + // 2-byte aligned members + uint16_t appearance_{0}; // 2 bytes + + // 1-byte aligned members (grouped together to minimize padding) + BLEComponentState state_{BLE_COMPONENT_STATE_OFF}; // 1 byte (uint8_t enum) + bool enable_on_boot_{}; // 1 byte }; // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/esp32_ble/ble_event.h b/esphome/components/esp32_ble/ble_event.h index dd3ec3da42..9268c710f3 100644 --- a/esphome/components/esp32_ble/ble_event.h +++ b/esphome/components/esp32_ble/ble_event.h @@ -134,13 +134,13 @@ class BLEEvent { } // Destructor to clean up heap allocations - ~BLEEvent() { this->cleanup_heap_data(); } + ~BLEEvent() { this->release(); } // Default constructor for pre-allocation in pool BLEEvent() : type_(GAP) {} - // Clean up any heap-allocated data - void cleanup_heap_data() { + // Invoked on return to EventPool - clean up any heap-allocated data + void release() { if (this->type_ == GAP) { return; } @@ -161,19 +161,19 @@ class BLEEvent { // Load new event data for reuse (replaces previous event data) void load_gap_event(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { - this->cleanup_heap_data(); + this->release(); this->type_ = GAP; this->init_gap_data_(e, p); } void load_gattc_event(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) { - this->cleanup_heap_data(); + this->release(); this->type_ = GATTC; this->init_gattc_data_(e, i, p); } void load_gatts_event(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) { - this->cleanup_heap_data(); + this->release(); this->type_ = GATTS; this->init_gatts_data_(e, i, p); } diff --git a/esphome/components/esp32_ble/ble_event_pool.h b/esphome/components/esp32_ble/ble_event_pool.h deleted file mode 100644 index ef123b1325..0000000000 --- a/esphome/components/esp32_ble/ble_event_pool.h +++ /dev/null @@ -1,72 +0,0 @@ -#pragma once - -#ifdef USE_ESP32 - -#include -#include -#include "ble_event.h" -#include "queue.h" -#include "esphome/core/helpers.h" - -namespace esphome { -namespace esp32_ble { - -// BLE Event Pool - On-demand pool of BLEEvent objects to avoid heap fragmentation -// Events are allocated on first use and reused thereafter, growing to peak usage -template class BLEEventPool { - public: - BLEEventPool() : total_created_(0) {} - - ~BLEEventPool() { - // Clean up any remaining events in the free list - BLEEvent *event; - while ((event = this->free_list_.pop()) != nullptr) { - delete event; - } - } - - // Allocate an event from the pool - // Returns nullptr if pool is full - BLEEvent *allocate() { - // Try to get from free list first - BLEEvent *event = this->free_list_.pop(); - if (event != nullptr) - return event; - - // Need to create a new event - if (this->total_created_ >= SIZE) { - // Pool is at capacity - return nullptr; - } - - // Use internal RAM for better performance - RAMAllocator allocator(RAMAllocator::ALLOC_INTERNAL); - event = allocator.allocate(1); - - if (event == nullptr) { - // Memory allocation failed - return nullptr; - } - - // Placement new to construct the object - new (event) BLEEvent(); - this->total_created_++; - return event; - } - - // Return an event to the pool for reuse - void release(BLEEvent *event) { - if (event != nullptr) { - this->free_list_.push(event); - } - } - - private: - LockFreeQueue free_list_; // Free events ready for reuse - uint8_t total_created_; // Total events created (high water mark) -}; - -} // namespace esp32_ble -} // namespace esphome - -#endif diff --git a/esphome/components/esp32_ble/queue.h b/esphome/components/esp32_ble/queue.h deleted file mode 100644 index 75bf1eef25..0000000000 --- a/esphome/components/esp32_ble/queue.h +++ /dev/null @@ -1,85 +0,0 @@ -#pragma once - -#ifdef USE_ESP32 - -#include -#include - -/* - * BLE events come in from a separate Task (thread) in the ESP32 stack. Rather - * than using mutex-based locking, this lock-free queue allows the BLE - * task to enqueue events without blocking. The main loop() then processes - * these events at a safer time. - * - * This is a Single-Producer Single-Consumer (SPSC) lock-free ring buffer. - * The BLE task is the only producer, and the main loop() is the only consumer. - */ - -namespace esphome { -namespace esp32_ble { - -template class LockFreeQueue { - public: - LockFreeQueue() : head_(0), tail_(0), dropped_count_(0) {} - - bool push(T *element) { - if (element == nullptr) - return false; - - uint8_t current_tail = tail_.load(std::memory_order_relaxed); - uint8_t next_tail = (current_tail + 1) % SIZE; - - if (next_tail == head_.load(std::memory_order_acquire)) { - // Buffer full - dropped_count_.fetch_add(1, std::memory_order_relaxed); - return false; - } - - buffer_[current_tail] = element; - tail_.store(next_tail, std::memory_order_release); - return true; - } - - T *pop() { - uint8_t current_head = head_.load(std::memory_order_relaxed); - - if (current_head == tail_.load(std::memory_order_acquire)) { - return nullptr; // Empty - } - - T *element = buffer_[current_head]; - head_.store((current_head + 1) % SIZE, std::memory_order_release); - return element; - } - - size_t size() const { - uint8_t tail = tail_.load(std::memory_order_acquire); - uint8_t head = head_.load(std::memory_order_acquire); - return (tail - head + SIZE) % SIZE; - } - - uint16_t get_and_reset_dropped_count() { return dropped_count_.exchange(0, std::memory_order_relaxed); } - - void increment_dropped_count() { dropped_count_.fetch_add(1, std::memory_order_relaxed); } - - bool empty() const { return head_.load(std::memory_order_acquire) == tail_.load(std::memory_order_acquire); } - - bool full() const { - uint8_t next_tail = (tail_.load(std::memory_order_relaxed) + 1) % SIZE; - return next_tail == head_.load(std::memory_order_acquire); - } - - protected: - T *buffer_[SIZE]; - // Atomic: written by producer (push/increment), read+reset by consumer (get_and_reset) - std::atomic dropped_count_; // 65535 max - more than enough for drop tracking - // Atomic: written by consumer (pop), read by producer (push) to check if full - std::atomic head_; - // Atomic: written by producer (push), read by consumer (pop) to check if empty - std::atomic tail_; -}; - -} // namespace esp32_ble -} // namespace esphome - -#endif diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index 4e61fb287c..7d0a3bbfd5 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -22,6 +22,16 @@ void BLEClientBase::setup() { this->connection_index_ = connection_index++; } +void BLEClientBase::set_state(espbt::ClientState st) { + ESP_LOGV(TAG, "[%d] [%s] Set state %d", this->connection_index_, this->address_str_.c_str(), (int) st); + ESPBTClient::set_state(st); + + if (st == espbt::ClientState::READY_TO_CONNECT) { + // Enable loop when we need to connect + this->enable_loop(); + } +} + void BLEClientBase::loop() { if (!esp32_ble::global_ble->is_active()) { this->set_state(espbt::ClientState::INIT); @@ -37,9 +47,14 @@ void BLEClientBase::loop() { } // READY_TO_CONNECT means we have discovered the device // and the scanner has been stopped by the tracker. - if (this->state_ == espbt::ClientState::READY_TO_CONNECT) { + else if (this->state_ == espbt::ClientState::READY_TO_CONNECT) { this->connect(); } + // If its idle, we can disable the loop as set_state + // will enable it again when we need to connect. + else if (this->state_ == espbt::ClientState::IDLE) { + this->disable_loop(); + } } float BLEClientBase::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; } @@ -481,17 +496,17 @@ float BLEClientBase::parse_char_value(uint8_t *value, uint16_t length) { if (length > 2) { return (float) encode_uint16(value[1], value[2]); } - // fall through + [[fallthrough]]; case 0x7: // uint24. if (length > 3) { return (float) encode_uint24(value[1], value[2], value[3]); } - // fall through + [[fallthrough]]; case 0x8: // uint32. if (length > 4) { return (float) encode_uint32(value[1], value[2], value[3], value[4]); } - // fall through + [[fallthrough]]; case 0xC: // int8. return (float) ((int8_t) value[1]); case 0xD: // int12. @@ -499,12 +514,12 @@ float BLEClientBase::parse_char_value(uint8_t *value, uint16_t length) { if (length > 2) { return (float) ((int16_t) (value[1] << 8) + (int16_t) value[2]); } - // fall through + [[fallthrough]]; case 0xF: // int24. if (length > 3) { return (float) ((int32_t) (value[1] << 16) + (int32_t) (value[2] << 8) + (int32_t) (value[3])); } - // fall through + [[fallthrough]]; case 0x10: // int32. if (length > 4) { return (float) ((int32_t) (value[1] << 24) + (int32_t) (value[2] << 16) + (int32_t) (value[3] << 8) + diff --git a/esphome/components/esp32_ble_client/ble_client_base.h b/esphome/components/esp32_ble_client/ble_client_base.h index 89ac04e38c..bf3b589b1b 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.h +++ b/esphome/components/esp32_ble_client/ble_client_base.h @@ -93,22 +93,37 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { bool check_addr(esp_bd_addr_t &addr) { return memcmp(addr, this->remote_bda_, sizeof(esp_bd_addr_t)) == 0; } + void set_state(espbt::ClientState st) override; + protected: - int gattc_if_; - esp_bd_addr_t remote_bda_; - esp_ble_addr_type_t remote_addr_type_{BLE_ADDR_TYPE_PUBLIC}; - uint16_t conn_id_{UNSET_CONN_ID}; + // Memory optimized layout for 32-bit systems + // Group 1: 8-byte types uint64_t address_{0}; - bool auto_connect_{false}; + + // Group 2: Container types (grouped for memory optimization) std::string address_str_{}; - uint8_t connection_index_; - int16_t service_count_{0}; - uint16_t mtu_{23}; - bool paired_{false}; - espbt::ConnectionType connection_type_{espbt::ConnectionType::V1}; std::vector services_; + + // Group 3: 4-byte types + int gattc_if_; esp_gatt_status_t status_{ESP_GATT_OK}; + // Group 4: Arrays (6 bytes) + esp_bd_addr_t remote_bda_; + + // Group 5: 2-byte types + uint16_t conn_id_{UNSET_CONN_ID}; + uint16_t mtu_{23}; + + // Group 6: 1-byte types and small enums + esp_ble_addr_type_t remote_addr_type_{BLE_ADDR_TYPE_PUBLIC}; + espbt::ConnectionType connection_type_{espbt::ConnectionType::V1}; + uint8_t connection_index_; + uint8_t service_count_{0}; // ESP32 has max handles < 255, typical devices have < 50 services + bool auto_connect_{false}; + bool paired_{false}; + // 6 bytes used, 2 bytes padding + void log_event_(const char *name); }; diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 2242d709a4..547cf84ed1 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -29,8 +29,6 @@ from esphome.const import ( CONF_ON_BLE_SERVICE_DATA_ADVERTISE, CONF_SERVICE_UUID, CONF_TRIGGER_ID, - KEY_CORE, - KEY_FRAMEWORK_VERSION, ) from esphome.core import CORE @@ -323,10 +321,7 @@ async def to_code(config): # https://github.com/espressif/esp-idf/issues/2503 # Match arduino CONFIG_BTU_TASK_STACK_SIZE # https://github.com/espressif/arduino-esp32/blob/fd72cf46ad6fc1a6de99c1d83ba8eba17d80a4ee/tools/sdk/esp32/sdkconfig#L1866 - if CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] >= cv.Version(4, 4, 6): - add_idf_sdkconfig_option("CONFIG_BT_BTU_TASK_STACK_SIZE", 8192) - else: - add_idf_sdkconfig_option("CONFIG_BTU_TASK_STACK_SIZE", 8192) + add_idf_sdkconfig_option("CONFIG_BT_BTU_TASK_STACK_SIZE", 8192) add_idf_sdkconfig_option("CONFIG_BT_ACL_CONNECTIONS", 9) add_idf_sdkconfig_option( "CONFIG_BTDM_CTRL_BLE_MAX_CONN", config[CONF_MAX_CONNECTIONS] @@ -335,8 +330,7 @@ async def to_code(config): # max notifications in 5.x, setting CONFIG_BT_ACL_CONNECTIONS # is enough in 4.x # https://github.com/esphome/issues/issues/6808 - if CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] >= cv.Version(5, 0, 0): - add_idf_sdkconfig_option("CONFIG_BT_GATTC_NOTIF_REG_MAX", 9) + add_idf_sdkconfig_option("CONFIG_BT_GATTC_NOTIF_REG_MAX", 9) cg.add_define("USE_OTA_STATE_CALLBACK") # To be notified when an OTA update starts cg.add_define("USE_ESP32_BLE_CLIENT") diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index a1fb727dd0..d950ccb5f1 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -122,10 +122,10 @@ void ESP32BLETracker::loop() { // Consumer side: This runs in the main loop thread if (this->scanner_state_ == ScannerState::RUNNING) { // Load our own index with relaxed ordering (we're the only writer) - size_t read_idx = this->ring_read_index_.load(std::memory_order_relaxed); + uint8_t read_idx = this->ring_read_index_.load(std::memory_order_relaxed); // Load producer's index with acquire to see their latest writes - size_t write_idx = this->ring_write_index_.load(std::memory_order_acquire); + uint8_t write_idx = this->ring_write_index_.load(std::memory_order_acquire); while (read_idx != write_idx) { // Process one result at a time directly from ring buffer @@ -409,11 +409,11 @@ void ESP32BLETracker::gap_scan_event_handler(const BLEScanResult &scan_result) { // IMPORTANT: Only this thread writes to ring_write_index_ // Load our own index with relaxed ordering (we're the only writer) - size_t write_idx = this->ring_write_index_.load(std::memory_order_relaxed); - size_t next_write_idx = (write_idx + 1) % SCAN_RESULT_BUFFER_SIZE; + uint8_t write_idx = this->ring_write_index_.load(std::memory_order_relaxed); + uint8_t next_write_idx = (write_idx + 1) % SCAN_RESULT_BUFFER_SIZE; // Load consumer's index with acquire to see their latest updates - size_t read_idx = this->ring_read_index_.load(std::memory_order_acquire); + uint8_t read_idx = this->ring_read_index_.load(std::memory_order_acquire); // Check if buffer is full if (next_write_idx != read_idx) { diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 892f76f49c..f5ed75a93e 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -133,7 +133,7 @@ class ESPBTDeviceListener { ESP32BLETracker *parent_{nullptr}; }; -enum class ClientState { +enum class ClientState : uint8_t { // Connection is allocated INIT, // Client is disconnecting @@ -169,7 +169,7 @@ enum class ScannerState { STOPPED, }; -enum class ConnectionType { +enum class ConnectionType : uint8_t { // The default connection type, we hold all the services in ram // for the duration of the connection. V1, @@ -197,15 +197,19 @@ class ESPBTClient : public ESPBTDeviceListener { } } ClientState state() const { return state_; } - int app_id; + + // Memory optimized layout + uint8_t app_id; // App IDs are small integers assigned sequentially protected: + // Group 1: 1-byte types ClientState state_{ClientState::INIT}; // want_disconnect_ is set to true when a disconnect is requested // while the client is connecting. This is used to disconnect the // client as soon as we get the connection id (conn_id_) from the // ESP_GATTC_OPEN_EVT event. bool want_disconnect_{false}; + // 2 bytes used, 2 bytes padding }; class ESP32BLETracker : public Component, @@ -266,7 +270,7 @@ class ESP32BLETracker : public Component, /// Called to set the scanner state. Will also call callbacks to let listeners know when state is changed. void set_scanner_state_(ScannerState state); - int app_id_{0}; + uint8_t app_id_{0}; /// Vector of addresses that have already been printed in print_bt_device_info std::vector already_discovered_; @@ -293,9 +297,9 @@ class ESP32BLETracker : public Component, // Consumer: ESPHome main loop (loop() method) // This design ensures zero blocking in the BT callback and prevents scan result loss BLEScanResult *scan_ring_buffer_; - std::atomic ring_write_index_{0}; // Written only by BT callback (producer) - std::atomic ring_read_index_{0}; // Written only by main loop (consumer) - std::atomic scan_results_dropped_{0}; // Tracks buffer overflow events + std::atomic ring_write_index_{0}; // Written only by BT callback (producer) + std::atomic ring_read_index_{0}; // Written only by main loop (consumer) + std::atomic scan_results_dropped_{0}; // Tracks buffer overflow events esp_bt_status_t scan_start_failed_{ESP_BT_STATUS_SUCCESS}; esp_bt_status_t scan_set_param_failed_{ESP_BT_STATUS_SUCCESS}; diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index b4038c1841..138f318a5d 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -1,5 +1,6 @@ from esphome import automation, pins import esphome.codegen as cg +from esphome.components import i2c from esphome.components.esp32 import add_idf_component import esphome.config_validation as cv from esphome.const import ( @@ -7,6 +8,7 @@ from esphome.const import ( CONF_CONTRAST, CONF_DATA_PINS, CONF_FREQUENCY, + CONF_I2C_ID, CONF_ID, CONF_PIN, CONF_RESET_PIN, @@ -17,11 +19,11 @@ from esphome.const import ( CONF_VSYNC_PIN, ) from esphome.core import CORE -from esphome.cpp_helpers import setup_entity +from esphome.core.entity_helpers import setup_entity DEPENDENCIES = ["esp32"] -AUTO_LOAD = ["psram"] +AUTO_LOAD = ["camera", "psram"] esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera") ESP32Camera = esp32_camera_ns.class_("ESP32Camera", cg.PollingComponent, cg.EntityBase) @@ -149,93 +151,104 @@ CONF_ON_IMAGE = "on_image" camera_range_param = cv.int_range(min=-2, max=2) -CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(ESP32Camera), - # pin assignment - cv.Required(CONF_DATA_PINS): cv.All( - [pins.internal_gpio_input_pin_number], cv.Length(min=8, max=8) - ), - cv.Required(CONF_VSYNC_PIN): pins.internal_gpio_input_pin_number, - cv.Required(CONF_HREF_PIN): pins.internal_gpio_input_pin_number, - cv.Required(CONF_PIXEL_CLOCK_PIN): pins.internal_gpio_input_pin_number, - cv.Required(CONF_EXTERNAL_CLOCK): cv.Schema( - { - cv.Required(CONF_PIN): pins.internal_gpio_input_pin_number, - cv.Optional(CONF_FREQUENCY, default="20MHz"): cv.All( - cv.frequency, cv.Range(min=8e6, max=20e6) - ), - } - ), - cv.Required(CONF_I2C_PINS): cv.Schema( - { - cv.Required(CONF_SDA): pins.internal_gpio_output_pin_number, - cv.Required(CONF_SCL): pins.internal_gpio_output_pin_number, - } - ), - cv.Optional(CONF_RESET_PIN): pins.internal_gpio_output_pin_number, - cv.Optional(CONF_POWER_DOWN_PIN): pins.internal_gpio_output_pin_number, - # image - cv.Optional(CONF_RESOLUTION, default="640X480"): cv.enum( - FRAME_SIZES, upper=True - ), - cv.Optional(CONF_JPEG_QUALITY, default=10): cv.int_range(min=6, max=63), - cv.Optional(CONF_CONTRAST, default=0): camera_range_param, - cv.Optional(CONF_BRIGHTNESS, default=0): camera_range_param, - cv.Optional(CONF_SATURATION, default=0): camera_range_param, - cv.Optional(CONF_VERTICAL_FLIP, default=True): cv.boolean, - cv.Optional(CONF_HORIZONTAL_MIRROR, default=True): cv.boolean, - cv.Optional(CONF_SPECIAL_EFFECT, default="NONE"): cv.enum( - ENUM_SPECIAL_EFFECT, upper=True - ), - # exposure - cv.Optional(CONF_AGC_MODE, default="AUTO"): cv.enum( - ENUM_GAIN_CONTROL_MODE, upper=True - ), - cv.Optional(CONF_AEC2, default=False): cv.boolean, - cv.Optional(CONF_AE_LEVEL, default=0): camera_range_param, - cv.Optional(CONF_AEC_VALUE, default=300): cv.int_range(min=0, max=1200), - # gains - cv.Optional(CONF_AEC_MODE, default="AUTO"): cv.enum( - ENUM_GAIN_CONTROL_MODE, upper=True - ), - cv.Optional(CONF_AGC_VALUE, default=0): cv.int_range(min=0, max=30), - cv.Optional(CONF_AGC_GAIN_CEILING, default="2X"): cv.enum( - ENUM_GAIN_CEILING, upper=True - ), - # white balance - cv.Optional(CONF_WB_MODE, default="AUTO"): cv.enum(ENUM_WB_MODE, upper=True), - # test pattern - cv.Optional(CONF_TEST_PATTERN, default=False): cv.boolean, - # framerates - cv.Optional(CONF_MAX_FRAMERATE, default="10 fps"): cv.All( - cv.framerate, cv.Range(min=0, min_included=False, max=60) - ), - cv.Optional(CONF_IDLE_FRAMERATE, default="0.1 fps"): cv.All( - cv.framerate, cv.Range(min=0, max=1) - ), - cv.Optional(CONF_FRAME_BUFFER_COUNT, default=1): cv.int_range(min=1, max=2), - cv.Optional(CONF_ON_STREAM_START): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - ESP32CameraStreamStartTrigger - ), - } - ), - cv.Optional(CONF_ON_STREAM_STOP): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - ESP32CameraStreamStopTrigger - ), - } - ), - cv.Optional(CONF_ON_IMAGE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESP32CameraImageTrigger), - } - ), - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = cv.All( + cv.ENTITY_BASE_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(ESP32Camera), + # pin assignment + cv.Required(CONF_DATA_PINS): cv.All( + [pins.internal_gpio_input_pin_number], cv.Length(min=8, max=8) + ), + cv.Required(CONF_VSYNC_PIN): pins.internal_gpio_input_pin_number, + cv.Required(CONF_HREF_PIN): pins.internal_gpio_input_pin_number, + cv.Required(CONF_PIXEL_CLOCK_PIN): pins.internal_gpio_input_pin_number, + cv.Required(CONF_EXTERNAL_CLOCK): cv.Schema( + { + cv.Required(CONF_PIN): pins.internal_gpio_input_pin_number, + cv.Optional(CONF_FREQUENCY, default="20MHz"): cv.All( + cv.frequency, cv.Range(min=8e6, max=20e6) + ), + } + ), + cv.Optional(CONF_I2C_PINS): cv.Schema( + { + cv.Required(CONF_SDA): pins.internal_gpio_output_pin_number, + cv.Required(CONF_SCL): pins.internal_gpio_output_pin_number, + } + ), + cv.Optional(CONF_I2C_ID): cv.Any( + cv.use_id(i2c.InternalI2CBus), + msg="I2C bus must be an internal ESP32 I2C bus", + ), + cv.Optional(CONF_RESET_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_POWER_DOWN_PIN): pins.internal_gpio_output_pin_number, + # image + cv.Optional(CONF_RESOLUTION, default="640X480"): cv.enum( + FRAME_SIZES, upper=True + ), + cv.Optional(CONF_JPEG_QUALITY, default=10): cv.int_range(min=6, max=63), + cv.Optional(CONF_CONTRAST, default=0): camera_range_param, + cv.Optional(CONF_BRIGHTNESS, default=0): camera_range_param, + cv.Optional(CONF_SATURATION, default=0): camera_range_param, + cv.Optional(CONF_VERTICAL_FLIP, default=True): cv.boolean, + cv.Optional(CONF_HORIZONTAL_MIRROR, default=True): cv.boolean, + cv.Optional(CONF_SPECIAL_EFFECT, default="NONE"): cv.enum( + ENUM_SPECIAL_EFFECT, upper=True + ), + # exposure + cv.Optional(CONF_AGC_MODE, default="AUTO"): cv.enum( + ENUM_GAIN_CONTROL_MODE, upper=True + ), + cv.Optional(CONF_AEC2, default=False): cv.boolean, + cv.Optional(CONF_AE_LEVEL, default=0): camera_range_param, + cv.Optional(CONF_AEC_VALUE, default=300): cv.int_range(min=0, max=1200), + # gains + cv.Optional(CONF_AEC_MODE, default="AUTO"): cv.enum( + ENUM_GAIN_CONTROL_MODE, upper=True + ), + cv.Optional(CONF_AGC_VALUE, default=0): cv.int_range(min=0, max=30), + cv.Optional(CONF_AGC_GAIN_CEILING, default="2X"): cv.enum( + ENUM_GAIN_CEILING, upper=True + ), + # white balance + cv.Optional(CONF_WB_MODE, default="AUTO"): cv.enum( + ENUM_WB_MODE, upper=True + ), + # test pattern + cv.Optional(CONF_TEST_PATTERN, default=False): cv.boolean, + # framerates + cv.Optional(CONF_MAX_FRAMERATE, default="10 fps"): cv.All( + cv.framerate, cv.Range(min=0, min_included=False, max=60) + ), + cv.Optional(CONF_IDLE_FRAMERATE, default="0.1 fps"): cv.All( + cv.framerate, cv.Range(min=0, max=1) + ), + cv.Optional(CONF_FRAME_BUFFER_COUNT, default=1): cv.int_range(min=1, max=2), + cv.Optional(CONF_ON_STREAM_START): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + ESP32CameraStreamStartTrigger + ), + } + ), + cv.Optional(CONF_ON_STREAM_STOP): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + ESP32CameraStreamStopTrigger + ), + } + ), + cv.Optional(CONF_ON_IMAGE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + ESP32CameraImageTrigger + ), + } + ), + } + ).extend(cv.COMPONENT_SCHEMA), + cv.has_exactly_one_key(CONF_I2C_PINS, CONF_I2C_ID), +) SETTERS = { # pin assignment @@ -270,8 +283,9 @@ SETTERS = { async def to_code(config): + cg.add_define("USE_CAMERA") var = cg.new_Pvariable(config[CONF_ID]) - await setup_entity(var, config) + await setup_entity(var, config, "camera") await cg.register_component(var, config) for key, setter in SETTERS.items(): @@ -280,8 +294,12 @@ async def to_code(config): extclk = config[CONF_EXTERNAL_CLOCK] cg.add(var.set_external_clock(extclk[CONF_PIN], extclk[CONF_FREQUENCY])) - i2c_pins = config[CONF_I2C_PINS] - cg.add(var.set_i2c_pins(i2c_pins[CONF_SDA], i2c_pins[CONF_SCL])) + if i2c_id := config.get(CONF_I2C_ID): + i2c_hub = await cg.get_variable(i2c_id) + cg.add(var.set_i2c_id(i2c_hub)) + else: + i2c_pins = config[CONF_I2C_PINS] + cg.add(var.set_i2c_pins(i2c_pins[CONF_SDA], i2c_pins[CONF_SCL])) cg.add(var.set_max_update_interval(1000 / config[CONF_MAX_FRAMERATE])) if config[CONF_IDLE_FRAMERATE] == 0: cg.add(var.set_idle_update_interval(0)) @@ -293,11 +311,7 @@ async def to_code(config): cg.add_define("USE_ESP32_CAMERA") if CORE.using_esp_idf: - add_idf_component( - name="esp32-camera", - repo="https://github.com/espressif/esp32-camera.git", - ref="v2.0.15", - ) + add_idf_component(name="espressif/esp32-camera", ref="2.0.15") for conf in config.get(CONF_ON_STREAM_START, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) diff --git a/esphome/components/esp32_camera/esp32_camera.cpp b/esphome/components/esp32_camera/esp32_camera.cpp index da0f277358..eadb8a4408 100644 --- a/esphome/components/esp32_camera/esp32_camera.cpp +++ b/esphome/components/esp32_camera/esp32_camera.cpp @@ -1,9 +1,9 @@ #ifdef USE_ESP32 #include "esp32_camera.h" -#include "esphome/core/log.h" -#include "esphome/core/hal.h" #include "esphome/core/application.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" #include @@ -14,7 +14,11 @@ static const char *const TAG = "esp32_camera"; /* ---------------- public API (derivated) ---------------- */ void ESP32Camera::setup() { - global_esp32_camera = this; +#ifdef USE_I2C + if (this->i2c_bus_ != nullptr) { + this->config_.sccb_i2c_port = this->i2c_bus_->get_port(); + } +#endif /* initialize time to now */ this->last_update_ = millis(); @@ -37,7 +41,7 @@ void ESP32Camera::setup() { xTaskCreatePinnedToCore(&ESP32Camera::framebuffer_task, "framebuffer_task", // name 1024, // stack size - nullptr, // task pv params + this, // task pv params 1, // priority nullptr, // handle 1 // core @@ -170,7 +174,7 @@ void ESP32Camera::loop() { const uint32_t now = App.get_loop_component_start_time(); if (this->idle_update_interval_ != 0 && now - this->last_idle_request_ > this->idle_update_interval_) { this->last_idle_request_ = now; - this->request_image(IDLE); + this->request_image(camera::IDLE); } // Check if we should fetch a new image @@ -196,7 +200,7 @@ void ESP32Camera::loop() { xQueueSend(this->framebuffer_return_queue_, &fb, portMAX_DELAY); return; } - this->current_image_ = std::make_shared(fb, this->single_requesters_ | this->stream_requesters_); + this->current_image_ = std::make_shared(fb, this->single_requesters_ | this->stream_requesters_); ESP_LOGD(TAG, "Got Image: len=%u", fb->len); this->new_image_callback_.call(this->current_image_); @@ -219,8 +223,6 @@ ESP32Camera::ESP32Camera() { this->config_.fb_count = 1; this->config_.grab_mode = CAMERA_GRAB_WHEN_EMPTY; this->config_.fb_location = CAMERA_FB_IN_PSRAM; - - global_esp32_camera = this; } /* ---------------- setters ---------------- */ @@ -246,6 +248,13 @@ void ESP32Camera::set_i2c_pins(uint8_t sda, uint8_t scl) { this->config_.pin_sccb_sda = sda; this->config_.pin_sccb_scl = scl; } +#ifdef USE_I2C +void ESP32Camera::set_i2c_id(i2c::InternalI2CBus *i2c_bus) { + this->i2c_bus_ = i2c_bus; + this->config_.pin_sccb_sda = -1; + this->config_.pin_sccb_scl = -1; +} +#endif // USE_I2C void ESP32Camera::set_reset_pin(uint8_t pin) { this->config_.pin_reset = pin; } void ESP32Camera::set_power_down_pin(uint8_t pin) { this->config_.pin_pwdn = pin; } @@ -343,7 +352,7 @@ void ESP32Camera::set_frame_buffer_count(uint8_t fb_count) { } /* ---------------- public API (specific) ---------------- */ -void ESP32Camera::add_image_callback(std::function)> &&callback) { +void ESP32Camera::add_image_callback(std::function)> &&callback) { this->new_image_callback_.add(std::move(callback)); } void ESP32Camera::add_stream_start_callback(std::function &&callback) { @@ -352,15 +361,16 @@ void ESP32Camera::add_stream_start_callback(std::function &&callback) { void ESP32Camera::add_stream_stop_callback(std::function &&callback) { this->stream_stop_callback_.add(std::move(callback)); } -void ESP32Camera::start_stream(CameraRequester requester) { +void ESP32Camera::start_stream(camera::CameraRequester requester) { this->stream_start_callback_.call(); this->stream_requesters_ |= (1U << requester); } -void ESP32Camera::stop_stream(CameraRequester requester) { +void ESP32Camera::stop_stream(camera::CameraRequester requester) { this->stream_stop_callback_.call(); this->stream_requesters_ &= ~(1U << requester); } -void ESP32Camera::request_image(CameraRequester requester) { this->single_requesters_ |= (1U << requester); } +void ESP32Camera::request_image(camera::CameraRequester requester) { this->single_requesters_ |= (1U << requester); } +camera::CameraImageReader *ESP32Camera::create_image_reader() { return new ESP32CameraImageReader; } void ESP32Camera::update_camera_parameters() { sensor_t *s = esp_camera_sensor_get(); /* update image */ @@ -389,39 +399,39 @@ void ESP32Camera::update_camera_parameters() { bool ESP32Camera::has_requested_image_() const { return this->single_requesters_ || this->stream_requesters_; } bool ESP32Camera::can_return_image_() const { return this->current_image_.use_count() == 1; } void ESP32Camera::framebuffer_task(void *pv) { + ESP32Camera *that = (ESP32Camera *) pv; while (true) { camera_fb_t *framebuffer = esp_camera_fb_get(); - xQueueSend(global_esp32_camera->framebuffer_get_queue_, &framebuffer, portMAX_DELAY); + xQueueSend(that->framebuffer_get_queue_, &framebuffer, portMAX_DELAY); // return is no-op for config with 1 fb - xQueueReceive(global_esp32_camera->framebuffer_return_queue_, &framebuffer, portMAX_DELAY); + xQueueReceive(that->framebuffer_return_queue_, &framebuffer, portMAX_DELAY); esp_camera_fb_return(framebuffer); } } -ESP32Camera *global_esp32_camera; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - -/* ---------------- CameraImageReader class ---------------- */ -void CameraImageReader::set_image(std::shared_ptr image) { - this->image_ = std::move(image); +/* ---------------- ESP32CameraImageReader class ----------- */ +void ESP32CameraImageReader::set_image(std::shared_ptr image) { + this->image_ = std::static_pointer_cast(image); this->offset_ = 0; } -size_t CameraImageReader::available() const { +size_t ESP32CameraImageReader::available() const { if (!this->image_) return 0; return this->image_->get_data_length() - this->offset_; } -void CameraImageReader::return_image() { this->image_.reset(); } -void CameraImageReader::consume_data(size_t consumed) { this->offset_ += consumed; } -uint8_t *CameraImageReader::peek_data_buffer() { return this->image_->get_data_buffer() + this->offset_; } +void ESP32CameraImageReader::return_image() { this->image_.reset(); } +void ESP32CameraImageReader::consume_data(size_t consumed) { this->offset_ += consumed; } +uint8_t *ESP32CameraImageReader::peek_data_buffer() { return this->image_->get_data_buffer() + this->offset_; } -/* ---------------- CameraImage class ---------------- */ -CameraImage::CameraImage(camera_fb_t *buffer, uint8_t requesters) : buffer_(buffer), requesters_(requesters) {} +/* ---------------- ESP32CameraImage class ----------- */ +ESP32CameraImage::ESP32CameraImage(camera_fb_t *buffer, uint8_t requesters) + : buffer_(buffer), requesters_(requesters) {} -camera_fb_t *CameraImage::get_raw_buffer() { return this->buffer_; } -uint8_t *CameraImage::get_data_buffer() { return this->buffer_->buf; } -size_t CameraImage::get_data_length() { return this->buffer_->len; } -bool CameraImage::was_requested_by(CameraRequester requester) const { +camera_fb_t *ESP32CameraImage::get_raw_buffer() { return this->buffer_; } +uint8_t *ESP32CameraImage::get_data_buffer() { return this->buffer_->buf; } +size_t ESP32CameraImage::get_data_length() { return this->buffer_->len; } +bool ESP32CameraImage::was_requested_by(camera::CameraRequester requester) const { return (this->requesters_ & (1 << requester)) != 0; } diff --git a/esphome/components/esp32_camera/esp32_camera.h b/esphome/components/esp32_camera/esp32_camera.h index d5fe48c2a7..8ce3faf039 100644 --- a/esphome/components/esp32_camera/esp32_camera.h +++ b/esphome/components/esp32_camera/esp32_camera.h @@ -2,22 +2,23 @@ #ifdef USE_ESP32 -#include "esphome/core/automation.h" -#include "esphome/core/component.h" -#include "esphome/core/entity_base.h" -#include "esphome/core/helpers.h" #include #include #include +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/components/camera/camera.h" +#include "esphome/core/helpers.h" + +#ifdef USE_I2C +#include "esphome/components/i2c/i2c_bus.h" +#endif // USE_I2C namespace esphome { namespace esp32_camera { class ESP32Camera; -/* ---------------- enum classes ---------------- */ -enum CameraRequester { IDLE, API_REQUESTER, WEB_REQUESTER }; - enum ESP32CameraFrameSize { ESP32_CAMERA_SIZE_160X120, // QQVGA ESP32_CAMERA_SIZE_176X144, // QCIF @@ -73,13 +74,13 @@ enum ESP32SpecialEffect { }; /* ---------------- CameraImage class ---------------- */ -class CameraImage { +class ESP32CameraImage : public camera::CameraImage { public: - CameraImage(camera_fb_t *buffer, uint8_t requester); + ESP32CameraImage(camera_fb_t *buffer, uint8_t requester); camera_fb_t *get_raw_buffer(); - uint8_t *get_data_buffer(); - size_t get_data_length(); - bool was_requested_by(CameraRequester requester) const; + uint8_t *get_data_buffer() override; + size_t get_data_length() override; + bool was_requested_by(camera::CameraRequester requester) const override; protected: camera_fb_t *buffer_; @@ -92,21 +93,21 @@ struct CameraImageData { }; /* ---------------- CameraImageReader class ---------------- */ -class CameraImageReader { +class ESP32CameraImageReader : public camera::CameraImageReader { public: - void set_image(std::shared_ptr image); - size_t available() const; - uint8_t *peek_data_buffer(); - void consume_data(size_t consumed); - void return_image(); + void set_image(std::shared_ptr image) override; + size_t available() const override; + uint8_t *peek_data_buffer() override; + void consume_data(size_t consumed) override; + void return_image() override; protected: - std::shared_ptr image_; + std::shared_ptr image_; size_t offset_{0}; }; /* ---------------- ESP32Camera class ---------------- */ -class ESP32Camera : public EntityBase, public Component { +class ESP32Camera : public camera::Camera { public: ESP32Camera(); @@ -118,6 +119,9 @@ class ESP32Camera : public EntityBase, public Component { void set_pixel_clock_pin(uint8_t pin); void set_external_clock(uint8_t pin, uint32_t frequency); void set_i2c_pins(uint8_t sda, uint8_t scl); +#ifdef USE_I2C + void set_i2c_id(i2c::InternalI2CBus *i2c_bus); +#endif // USE_I2C void set_reset_pin(uint8_t pin); void set_power_down_pin(uint8_t pin); /* -- image */ @@ -155,14 +159,15 @@ class ESP32Camera : public EntityBase, public Component { void dump_config() override; float get_setup_priority() const override; /* public API (specific) */ - void start_stream(CameraRequester requester); - void stop_stream(CameraRequester requester); - void request_image(CameraRequester requester); + void start_stream(camera::CameraRequester requester) override; + void stop_stream(camera::CameraRequester requester) override; + void request_image(camera::CameraRequester requester) override; void update_camera_parameters(); - void add_image_callback(std::function)> &&callback); + void add_image_callback(std::function)> &&callback) override; void add_stream_start_callback(std::function &&callback); void add_stream_stop_callback(std::function &&callback); + camera::CameraImageReader *create_image_reader() override; protected: /* internal methods */ @@ -199,26 +204,26 @@ class ESP32Camera : public EntityBase, public Component { uint32_t idle_update_interval_{15000}; esp_err_t init_error_{ESP_OK}; - std::shared_ptr current_image_; + std::shared_ptr current_image_; uint8_t single_requesters_{0}; uint8_t stream_requesters_{0}; QueueHandle_t framebuffer_get_queue_; QueueHandle_t framebuffer_return_queue_; - CallbackManager)> new_image_callback_{}; + CallbackManager)> new_image_callback_{}; CallbackManager stream_start_callback_{}; CallbackManager stream_stop_callback_{}; uint32_t last_idle_request_{0}; uint32_t last_update_{0}; +#ifdef USE_I2C + i2c::InternalI2CBus *i2c_bus_{nullptr}; +#endif // USE_I2C }; -// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) -extern ESP32Camera *global_esp32_camera; - class ESP32CameraImageTrigger : public Trigger { public: explicit ESP32CameraImageTrigger(ESP32Camera *parent) { - parent->add_image_callback([this](const std::shared_ptr &image) { + parent->add_image_callback([this](const std::shared_ptr &image) { CameraImageData camera_image_data{}; camera_image_data.length = image->get_data_length(); camera_image_data.data = image->get_data_buffer(); diff --git a/esphome/components/esp32_camera_web_server/__init__.py b/esphome/components/esp32_camera_web_server/__init__.py index 363218bbac..a6a7ac3630 100644 --- a/esphome/components/esp32_camera_web_server/__init__.py +++ b/esphome/components/esp32_camera_web_server/__init__.py @@ -3,7 +3,8 @@ import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_MODE, CONF_PORT CODEOWNERS = ["@ayufan"] -DEPENDENCIES = ["esp32_camera"] +AUTO_LOAD = ["camera"] +DEPENDENCIES = ["network"] MULTI_CONF = True esp32_camera_web_server_ns = cg.esphome_ns.namespace("esp32_camera_web_server") diff --git a/esphome/components/esp32_camera_web_server/camera_web_server.cpp b/esphome/components/esp32_camera_web_server/camera_web_server.cpp index 0a83128908..1b81989296 100644 --- a/esphome/components/esp32_camera_web_server/camera_web_server.cpp +++ b/esphome/components/esp32_camera_web_server/camera_web_server.cpp @@ -40,7 +40,7 @@ CameraWebServer::CameraWebServer() {} CameraWebServer::~CameraWebServer() {} void CameraWebServer::setup() { - if (!esp32_camera::global_esp32_camera || esp32_camera::global_esp32_camera->is_failed()) { + if (!camera::Camera::instance() || camera::Camera::instance()->is_failed()) { this->mark_failed(); return; } @@ -67,8 +67,8 @@ void CameraWebServer::setup() { httpd_register_uri_handler(this->httpd_, &uri); - esp32_camera::global_esp32_camera->add_image_callback([this](std::shared_ptr image) { - if (this->running_ && image->was_requested_by(esp32_camera::WEB_REQUESTER)) { + camera::Camera::instance()->add_image_callback([this](std::shared_ptr image) { + if (this->running_ && image->was_requested_by(camera::WEB_REQUESTER)) { this->image_ = std::move(image); xSemaphoreGive(this->semaphore_); } @@ -108,8 +108,8 @@ void CameraWebServer::loop() { } } -std::shared_ptr CameraWebServer::wait_for_image_() { - std::shared_ptr image; +std::shared_ptr CameraWebServer::wait_for_image_() { + std::shared_ptr image; image.swap(this->image_); if (!image) { @@ -172,7 +172,7 @@ esp_err_t CameraWebServer::streaming_handler_(struct httpd_req *req) { uint32_t last_frame = millis(); uint32_t frames = 0; - esp32_camera::global_esp32_camera->start_stream(esphome::esp32_camera::WEB_REQUESTER); + camera::Camera::instance()->start_stream(esphome::camera::WEB_REQUESTER); while (res == ESP_OK && this->running_) { auto image = this->wait_for_image_(); @@ -205,7 +205,7 @@ esp_err_t CameraWebServer::streaming_handler_(struct httpd_req *req) { res = httpd_send_all(req, STREAM_ERROR, strlen(STREAM_ERROR)); } - esp32_camera::global_esp32_camera->stop_stream(esphome::esp32_camera::WEB_REQUESTER); + camera::Camera::instance()->stop_stream(esphome::camera::WEB_REQUESTER); ESP_LOGI(TAG, "STREAM: closed. Frames: %" PRIu32, frames); @@ -215,7 +215,7 @@ esp_err_t CameraWebServer::streaming_handler_(struct httpd_req *req) { esp_err_t CameraWebServer::snapshot_handler_(struct httpd_req *req) { esp_err_t res = ESP_OK; - esp32_camera::global_esp32_camera->request_image(esphome::esp32_camera::WEB_REQUESTER); + camera::Camera::instance()->request_image(esphome::camera::WEB_REQUESTER); auto image = this->wait_for_image_(); diff --git a/esphome/components/esp32_camera_web_server/camera_web_server.h b/esphome/components/esp32_camera_web_server/camera_web_server.h index 3ba8f31dd7..e70246745c 100644 --- a/esphome/components/esp32_camera_web_server/camera_web_server.h +++ b/esphome/components/esp32_camera_web_server/camera_web_server.h @@ -6,7 +6,7 @@ #include #include -#include "esphome/components/esp32_camera/esp32_camera.h" +#include "esphome/components/camera/camera.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "esphome/core/preferences.h" @@ -32,7 +32,7 @@ class CameraWebServer : public Component { void loop() override; protected: - std::shared_ptr wait_for_image_(); + std::shared_ptr wait_for_image_(); esp_err_t handler_(struct httpd_req *req); esp_err_t streaming_handler_(struct httpd_req *req); esp_err_t snapshot_handler_(struct httpd_req *req); @@ -40,7 +40,7 @@ class CameraWebServer : public Component { uint16_t port_{0}; void *httpd_{nullptr}; SemaphoreHandle_t semaphore_; - std::shared_ptr image_; + std::shared_ptr image_; bool running_{false}; Mode mode_{STREAM}; }; diff --git a/esphome/components/esp32_hall/esp32_hall.cpp b/esphome/components/esp32_hall/esp32_hall.cpp deleted file mode 100644 index 762497aedc..0000000000 --- a/esphome/components/esp32_hall/esp32_hall.cpp +++ /dev/null @@ -1,25 +0,0 @@ -#ifdef USE_ESP32 -#include "esp32_hall.h" -#include "esphome/core/log.h" -#include "esphome/core/hal.h" -#include - -namespace esphome { -namespace esp32_hall { - -static const char *const TAG = "esp32_hall"; - -void ESP32HallSensor::update() { - adc1_config_width(ADC_WIDTH_BIT_12); - int value_int = hall_sensor_read(); - float value = (value_int / 4095.0f) * 10000.0f; - ESP_LOGD(TAG, "'%s': Got reading %.0f µT", this->name_.c_str(), value); - this->publish_state(value); -} -std::string ESP32HallSensor::unique_id() { return get_mac_address() + "-hall"; } -void ESP32HallSensor::dump_config() { LOG_SENSOR("", "ESP32 Hall Sensor", this); } - -} // namespace esp32_hall -} // namespace esphome - -#endif diff --git a/esphome/components/esp32_hall/esp32_hall.h b/esphome/components/esp32_hall/esp32_hall.h deleted file mode 100644 index 8db50c4667..0000000000 --- a/esphome/components/esp32_hall/esp32_hall.h +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once - -#include "esphome/core/component.h" -#include "esphome/components/sensor/sensor.h" - -#ifdef USE_ESP32 - -namespace esphome { -namespace esp32_hall { - -class ESP32HallSensor : public sensor::Sensor, public PollingComponent { - public: - void dump_config() override; - - void update() override; - - std::string unique_id() override; -}; - -} // namespace esp32_hall -} // namespace esphome - -#endif diff --git a/esphome/components/esp32_hall/sensor.py b/esphome/components/esp32_hall/sensor.py index e7953d4b3d..b644389d3b 100644 --- a/esphome/components/esp32_hall/sensor.py +++ b/esphome/components/esp32_hall/sensor.py @@ -1,24 +1,5 @@ -import esphome.codegen as cg -from esphome.components import sensor import esphome.config_validation as cv -from esphome.const import ICON_MAGNET, STATE_CLASS_MEASUREMENT, UNIT_MICROTESLA -DEPENDENCIES = ["esp32"] - -esp32_hall_ns = cg.esphome_ns.namespace("esp32_hall") -ESP32HallSensor = esp32_hall_ns.class_( - "ESP32HallSensor", sensor.Sensor, cg.PollingComponent +CONFIG_SCHEMA = cv.invalid( + "The esp32_hall component has been removed as of ESPHome 2025.7.0. See https://github.com/esphome/esphome/pull/9117 for details." ) - -CONFIG_SCHEMA = sensor.sensor_schema( - ESP32HallSensor, - unit_of_measurement=UNIT_MICROTESLA, - icon=ICON_MAGNET, - accuracy_decimals=1, - state_class=STATE_CLASS_MEASUREMENT, -).extend(cv.polling_component_schema("60s")) - - -async def to_code(config): - var = await sensor.new_sensor(config) - await cg.register_component(var, config) diff --git a/esphome/components/esp32_hosted/__init__.py b/esphome/components/esp32_hosted/__init__.py new file mode 100644 index 0000000000..330800df12 --- /dev/null +++ b/esphome/components/esp32_hosted/__init__.py @@ -0,0 +1,101 @@ +import os + +from esphome import pins +from esphome.components import esp32 +import esphome.config_validation as cv +from esphome.const import ( + CONF_CLK_PIN, + CONF_RESET_PIN, + CONF_VARIANT, + KEY_CORE, + KEY_FRAMEWORK_VERSION, +) +from esphome.core import CORE + +CODEOWNERS = ["@swoboda1337"] + +CONF_ACTIVE_HIGH = "active_high" +CONF_CMD_PIN = "cmd_pin" +CONF_D0_PIN = "d0_pin" +CONF_D1_PIN = "d1_pin" +CONF_D2_PIN = "d2_pin" +CONF_D3_PIN = "d3_pin" +CONF_SLOT = "slot" + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.Required(CONF_VARIANT): cv.one_of(*esp32.VARIANTS, upper=True), + cv.Required(CONF_ACTIVE_HIGH): cv.boolean, + cv.Required(CONF_CLK_PIN): pins.internal_gpio_output_pin_number, + cv.Required(CONF_CMD_PIN): pins.internal_gpio_output_pin_number, + cv.Required(CONF_D0_PIN): pins.internal_gpio_output_pin_number, + cv.Required(CONF_D1_PIN): pins.internal_gpio_output_pin_number, + cv.Required(CONF_D2_PIN): pins.internal_gpio_output_pin_number, + cv.Required(CONF_D3_PIN): pins.internal_gpio_output_pin_number, + cv.Required(CONF_RESET_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_SLOT, default=1): cv.int_range(min=0, max=1), + } + ), +) + + +async def to_code(config): + if config[CONF_ACTIVE_HIGH]: + esp32.add_idf_sdkconfig_option( + "CONFIG_ESP_HOSTED_SDIO_RESET_ACTIVE_HIGH", + True, + ) + else: + esp32.add_idf_sdkconfig_option( + "CONFIG_ESP_HOSTED_SDIO_RESET_ACTIVE_LOW", + True, + ) + esp32.add_idf_sdkconfig_option( + "CONFIG_ESP_HOSTED_SDIO_GPIO_RESET_SLAVE", # NOLINT + config[CONF_RESET_PIN], + ) + esp32.add_idf_sdkconfig_option( + f"CONFIG_SLAVE_IDF_TARGET_{config[CONF_VARIANT]}", # NOLINT + True, + ) + esp32.add_idf_sdkconfig_option( + f"CONFIG_ESP_HOSTED_SDIO_SLOT_{config[CONF_SLOT]}", + True, + ) + esp32.add_idf_sdkconfig_option( + f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_CLK_SLOT_{config[CONF_SLOT]}", + config[CONF_CLK_PIN], + ) + esp32.add_idf_sdkconfig_option( + f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_CMD_SLOT_{config[CONF_SLOT]}", + config[CONF_CMD_PIN], + ) + esp32.add_idf_sdkconfig_option( + f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D0_SLOT_{config[CONF_SLOT]}", + config[CONF_D0_PIN], + ) + esp32.add_idf_sdkconfig_option( + f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D1_4BIT_BUS_SLOT_{config[CONF_SLOT]}", + config[CONF_D1_PIN], + ) + esp32.add_idf_sdkconfig_option( + f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D2_4BIT_BUS_SLOT_{config[CONF_SLOT]}", + config[CONF_D2_PIN], + ) + esp32.add_idf_sdkconfig_option( + f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D3_4BIT_BUS_SLOT_{config[CONF_SLOT]}", + config[CONF_D3_PIN], + ) + esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_CUSTOM_SDIO_PINS", True) + + framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] + os.environ["ESP_IDF_VERSION"] = f"{framework_ver.major}.{framework_ver.minor}" + esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.10.2") + esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0") + esp32.add_idf_component(name="espressif/esp_hosted", ref="2.0.11") + esp32.add_extra_script( + "post", + "esp32_hosted.py", + os.path.join(os.path.dirname(__file__), "esp32_hosted.py.script"), + ) diff --git a/esphome/components/esp32_hosted/esp32_hosted.py.script b/esphome/components/esp32_hosted/esp32_hosted.py.script new file mode 100644 index 0000000000..4be297c500 --- /dev/null +++ b/esphome/components/esp32_hosted/esp32_hosted.py.script @@ -0,0 +1,12 @@ +# pylint: disable=E0602 +Import("env") # noqa + +# Workaround whole archive issue +if "__LIB_DEPS" in env and "libespressif__esp_hosted.a" in env["__LIB_DEPS"]: + env.Append( + LINKFLAGS=[ + "-Wl,--whole-archive", + env["BUILD_DIR"] + "/esp-idf/espressif__esp_hosted/libespressif__esp_hosted.a", + "-Wl,--no-whole-archive", + ] + ) diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index 9d84d38968..d41094fda1 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -168,6 +168,8 @@ void ESP32ImprovComponent::loop() { case improv::STATE_PROVISIONED: { this->incoming_data_.clear(); this->set_status_indicator_state_(false); + // Provisioning complete, no further loop execution needed + this->disable_loop(); break; } } @@ -254,6 +256,7 @@ void ESP32ImprovComponent::start() { ESP_LOGD(TAG, "Setting Improv to start"); this->should_start_ = true; + this->enable_loop(); } void ESP32ImprovComponent::stop() { diff --git a/esphome/components/esp32_rmt/__init__.py b/esphome/components/esp32_rmt/__init__.py index 171c335727..1e72185e3e 100644 --- a/esphome/components/esp32_rmt/__init__.py +++ b/esphome/components/esp32_rmt/__init__.py @@ -1,48 +1,8 @@ -import esphome.codegen as cg from esphome.components import esp32 import esphome.config_validation as cv -from esphome.const import KEY_CORE, KEY_FRAMEWORK_VERSION -from esphome.core import CORE CODEOWNERS = ["@jesserockz"] -RMT_TX_CHANNELS = { - esp32.const.VARIANT_ESP32: [0, 1, 2, 3, 4, 5, 6, 7], - esp32.const.VARIANT_ESP32S2: [0, 1, 2, 3], - esp32.const.VARIANT_ESP32S3: [0, 1, 2, 3], - esp32.const.VARIANT_ESP32C3: [0, 1], - esp32.const.VARIANT_ESP32C6: [0, 1], - esp32.const.VARIANT_ESP32H2: [0, 1], -} - -RMT_RX_CHANNELS = { - esp32.const.VARIANT_ESP32: [0, 1, 2, 3, 4, 5, 6, 7], - esp32.const.VARIANT_ESP32S2: [0, 1, 2, 3], - esp32.const.VARIANT_ESP32S3: [4, 5, 6, 7], - esp32.const.VARIANT_ESP32C3: [2, 3], - esp32.const.VARIANT_ESP32C6: [2, 3], - esp32.const.VARIANT_ESP32H2: [2, 3], -} - -rmt_channel_t = cg.global_ns.enum("rmt_channel_t") -RMT_CHANNEL_ENUMS = { - 0: rmt_channel_t.RMT_CHANNEL_0, - 1: rmt_channel_t.RMT_CHANNEL_1, - 2: rmt_channel_t.RMT_CHANNEL_2, - 3: rmt_channel_t.RMT_CHANNEL_3, - 4: rmt_channel_t.RMT_CHANNEL_4, - 5: rmt_channel_t.RMT_CHANNEL_5, - 6: rmt_channel_t.RMT_CHANNEL_6, - 7: rmt_channel_t.RMT_CHANNEL_7, -} - - -def use_new_rmt_driver(): - framework_version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] - if CORE.using_esp_idf and framework_version >= cv.Version(5, 0, 0): - return True - return False - def validate_clock_resolution(): def _validator(value): @@ -60,21 +20,3 @@ def validate_clock_resolution(): return value return _validator - - -def validate_rmt_channel(*, tx: bool): - rmt_channels = RMT_TX_CHANNELS if tx else RMT_RX_CHANNELS - - def _validator(value): - cv.only_on_esp32(value) - value = cv.int_(value) - variant = esp32.get_esp32_variant() - if variant not in rmt_channels: - raise cv.Invalid(f"ESP32 variant {variant} does not support RMT.") - if value not in rmt_channels[variant]: - raise cv.Invalid( - f"RMT channel {value} does not support {'transmitting' if tx else 'receiving'} for ESP32 variant {variant}." - ) - return cv.enum(RMT_CHANNEL_ENUMS)(value) - - return _validator diff --git a/esphome/components/esp32_rmt_led_strip/led_strip.cpp b/esphome/components/esp32_rmt_led_strip/led_strip.cpp index 88ddf24d49..389c32882b 100644 --- a/esphome/components/esp32_rmt_led_strip/led_strip.cpp +++ b/esphome/components/esp32_rmt_led_strip/led_strip.cpp @@ -21,6 +21,43 @@ static const uint32_t RMT_CLK_FREQ = 80000000; static const uint8_t RMT_CLK_DIV = 2; #endif +static const size_t RMT_SYMBOLS_PER_BYTE = 8; + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) +static size_t IRAM_ATTR HOT encoder_callback(const void *data, size_t size, size_t symbols_written, size_t symbols_free, + rmt_symbol_word_t *symbols, bool *done, void *arg) { + auto *params = static_cast(arg); + const auto *bytes = static_cast(data); + size_t index = symbols_written / RMT_SYMBOLS_PER_BYTE; + + // convert byte to symbols + if (index < size) { + if (symbols_free < RMT_SYMBOLS_PER_BYTE) { + return 0; + } + for (int32_t i = 0; i < RMT_SYMBOLS_PER_BYTE; i++) { + if (bytes[index] & (1 << (7 - i))) { + symbols[i] = params->bit1; + } else { + symbols[i] = params->bit0; + } + } + if ((index + 1) >= size && params->reset.duration0 == 0 && params->reset.duration1 == 0) { + *done = true; + } + return RMT_SYMBOLS_PER_BYTE; + } + + // send reset + if (symbols_free < 1) { + return 0; + } + symbols[0] = params->reset; + *done = true; + return 1; +} +#endif + void ESP32RMTLEDStripLightOutput::setup() { ESP_LOGCONFIG(TAG, "Running setup"); @@ -42,11 +79,15 @@ void ESP32RMTLEDStripLightOutput::setup() { return; } -#if ESP_IDF_VERSION_MAJOR >= 5 +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) + // copy of the led buffer + this->rmt_buf_ = allocator.allocate(buffer_size); +#else RAMAllocator rmt_allocator(this->use_psram_ ? 0 : RAMAllocator::ALLOC_INTERNAL); // 8 bits per byte, 1 rmt_symbol_word_t per bit + 1 rmt_symbol_word_t for reset this->rmt_buf_ = rmt_allocator.allocate(buffer_size * 8 + 1); +#endif rmt_tx_channel_config_t channel; memset(&channel, 0, sizeof(channel)); @@ -66,6 +107,18 @@ void ESP32RMTLEDStripLightOutput::setup() { return; } +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) + rmt_simple_encoder_config_t encoder; + memset(&encoder, 0, sizeof(encoder)); + encoder.callback = encoder_callback; + encoder.arg = &this->params_; + encoder.min_chunk_size = 8; + if (rmt_new_simple_encoder(&encoder, &this->encoder_) != ESP_OK) { + ESP_LOGE(TAG, "Encoder creation failed"); + this->mark_failed(); + return; + } +#else rmt_copy_encoder_config_t encoder; memset(&encoder, 0, sizeof(encoder)); if (rmt_new_copy_encoder(&encoder, &this->encoder_) != ESP_OK) { @@ -73,42 +126,13 @@ void ESP32RMTLEDStripLightOutput::setup() { this->mark_failed(); return; } +#endif if (rmt_enable(this->channel_) != ESP_OK) { ESP_LOGE(TAG, "Enabling channel failed"); this->mark_failed(); return; } -#else - RAMAllocator rmt_allocator(this->use_psram_ ? 0 : RAMAllocator::ALLOC_INTERNAL); - - // 8 bits per byte, 1 rmt_item32_t per bit + 1 rmt_item32_t for reset - this->rmt_buf_ = rmt_allocator.allocate(buffer_size * 8 + 1); - - rmt_config_t config; - memset(&config, 0, sizeof(config)); - config.channel = this->channel_; - config.rmt_mode = RMT_MODE_TX; - config.gpio_num = gpio_num_t(this->pin_); - config.mem_block_num = 1; - config.clk_div = RMT_CLK_DIV; - config.tx_config.loop_en = false; - config.tx_config.carrier_level = RMT_CARRIER_LEVEL_LOW; - config.tx_config.carrier_en = false; - config.tx_config.idle_level = RMT_IDLE_LEVEL_LOW; - config.tx_config.idle_output_en = true; - - if (rmt_config(&config) != ESP_OK) { - ESP_LOGE(TAG, "Cannot initialize RMT!"); - this->mark_failed(); - return; - } - if (rmt_driver_install(config.channel, 0, 0) != ESP_OK) { - ESP_LOGE(TAG, "Cannot install RMT driver!"); - this->mark_failed(); - return; - } -#endif } void ESP32RMTLEDStripLightOutput::set_led_params(uint32_t bit0_high, uint32_t bit0_low, uint32_t bit1_high, @@ -116,20 +140,20 @@ void ESP32RMTLEDStripLightOutput::set_led_params(uint32_t bit0_high, uint32_t bi float ratio = (float) RMT_CLK_FREQ / RMT_CLK_DIV / 1e09f; // 0-bit - this->bit0_.duration0 = (uint32_t) (ratio * bit0_high); - this->bit0_.level0 = 1; - this->bit0_.duration1 = (uint32_t) (ratio * bit0_low); - this->bit0_.level1 = 0; + this->params_.bit0.duration0 = (uint32_t) (ratio * bit0_high); + this->params_.bit0.level0 = 1; + this->params_.bit0.duration1 = (uint32_t) (ratio * bit0_low); + this->params_.bit0.level1 = 0; // 1-bit - this->bit1_.duration0 = (uint32_t) (ratio * bit1_high); - this->bit1_.level0 = 1; - this->bit1_.duration1 = (uint32_t) (ratio * bit1_low); - this->bit1_.level1 = 0; + this->params_.bit1.duration0 = (uint32_t) (ratio * bit1_high); + this->params_.bit1.level0 = 1; + this->params_.bit1.duration1 = (uint32_t) (ratio * bit1_low); + this->params_.bit1.level1 = 0; // reset - this->reset_.duration0 = (uint32_t) (ratio * reset_time_high); - this->reset_.level0 = 1; - this->reset_.duration1 = (uint32_t) (ratio * reset_time_low); - this->reset_.level1 = 0; + this->params_.reset.duration0 = (uint32_t) (ratio * reset_time_high); + this->params_.reset.level0 = 1; + this->params_.reset.duration1 = (uint32_t) (ratio * reset_time_low); + this->params_.reset.level1 = 0; } void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) { @@ -145,11 +169,7 @@ void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) { ESP_LOGVV(TAG, "Writing RGB values to bus"); -#if ESP_IDF_VERSION_MAJOR >= 5 esp_err_t error = rmt_tx_wait_all_done(this->channel_, 1000); -#else - esp_err_t error = rmt_wait_tx_done(this->channel_, pdMS_TO_TICKS(1000)); -#endif if (error != ESP_OK) { ESP_LOGE(TAG, "RMT TX timeout"); this->status_set_warning(); @@ -157,20 +177,19 @@ void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) { } delayMicroseconds(50); +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) + memcpy(this->rmt_buf_, this->buf_, this->get_buffer_size_()); +#else size_t buffer_size = this->get_buffer_size_(); size_t size = 0; size_t len = 0; uint8_t *psrc = this->buf_; -#if ESP_IDF_VERSION_MAJOR >= 5 rmt_symbol_word_t *pdest = this->rmt_buf_; -#else - rmt_item32_t *pdest = this->rmt_buf_; -#endif while (size < buffer_size) { uint8_t b = *psrc; for (int i = 0; i < 8; i++) { - pdest->val = b & (1 << (7 - i)) ? this->bit1_.val : this->bit0_.val; + pdest->val = b & (1 << (7 - i)) ? this->params_.bit1.val : this->params_.bit0.val; pdest++; len++; } @@ -178,20 +197,19 @@ void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) { psrc++; } - if (this->reset_.duration0 > 0 || this->reset_.duration1 > 0) { - pdest->val = this->reset_.val; + if (this->params_.reset.duration0 > 0 || this->params_.reset.duration1 > 0) { + pdest->val = this->params_.reset.val; pdest++; len++; } +#endif -#if ESP_IDF_VERSION_MAJOR >= 5 rmt_transmit_config_t config; memset(&config, 0, sizeof(config)); - config.loop_count = 0; - config.flags.eot_level = 0; - error = rmt_transmit(this->channel_, this->encoder_, this->rmt_buf_, len * sizeof(rmt_symbol_word_t), &config); +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) + error = rmt_transmit(this->channel_, this->encoder_, this->rmt_buf_, this->get_buffer_size_(), &config); #else - error = rmt_write_items(this->channel_, this->rmt_buf_, len, false); + error = rmt_transmit(this->channel_, this->encoder_, this->rmt_buf_, len * sizeof(rmt_symbol_word_t), &config); #endif if (error != ESP_OK) { ESP_LOGE(TAG, "RMT TX error"); @@ -251,11 +269,7 @@ void ESP32RMTLEDStripLightOutput::dump_config() { "ESP32 RMT LED Strip:\n" " Pin: %u", this->pin_); -#if ESP_IDF_VERSION_MAJOR >= 5 ESP_LOGCONFIG(TAG, " RMT Symbols: %" PRIu32, this->rmt_symbols_); -#else - ESP_LOGCONFIG(TAG, " Channel: %u", this->channel_); -#endif const char *rgb_order; switch (this->rgb_order_) { case ORDER_RGB: diff --git a/esphome/components/esp32_rmt_led_strip/led_strip.h b/esphome/components/esp32_rmt_led_strip/led_strip.h index f0cec9b291..72ce659b4f 100644 --- a/esphome/components/esp32_rmt_led_strip/led_strip.h +++ b/esphome/components/esp32_rmt_led_strip/led_strip.h @@ -11,12 +11,7 @@ #include #include #include - -#if ESP_IDF_VERSION_MAJOR >= 5 #include -#else -#include -#endif namespace esphome { namespace esp32_rmt_led_strip { @@ -30,6 +25,12 @@ enum RGBOrder : uint8_t { ORDER_BRG, }; +struct LedParams { + rmt_symbol_word_t bit0; + rmt_symbol_word_t bit1; + rmt_symbol_word_t reset; +}; + class ESP32RMTLEDStripLightOutput : public light::AddressableLight { public: void setup() override; @@ -61,11 +62,7 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight { uint32_t reset_time_high, uint32_t reset_time_low); void set_rgb_order(RGBOrder rgb_order) { this->rgb_order_ = rgb_order; } -#if ESP_IDF_VERSION_MAJOR >= 5 void set_rmt_symbols(uint32_t rmt_symbols) { this->rmt_symbols_ = rmt_symbols; } -#else - void set_rmt_channel(rmt_channel_t channel) { this->channel_ = channel; } -#endif void clear_effect_data() override { for (int i = 0; i < this->size(); i++) @@ -81,18 +78,15 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight { uint8_t *buf_{nullptr}; uint8_t *effect_data_{nullptr}; -#if ESP_IDF_VERSION_MAJOR >= 5 + LedParams params_; rmt_channel_handle_t channel_{nullptr}; rmt_encoder_handle_t encoder_{nullptr}; - rmt_symbol_word_t *rmt_buf_{nullptr}; - rmt_symbol_word_t bit0_, bit1_, reset_; - uint32_t rmt_symbols_{48}; +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) + uint8_t *rmt_buf_{nullptr}; #else - rmt_item32_t *rmt_buf_{nullptr}; - rmt_item32_t bit0_, bit1_, reset_; - rmt_channel_t channel_{RMT_CHANNEL_0}; + rmt_symbol_word_t *rmt_buf_{nullptr}; #endif - + uint32_t rmt_symbols_{48}; uint8_t pin_; uint16_t num_leds_; bool is_rgbw_{false}; diff --git a/esphome/components/esp32_rmt_led_strip/light.py b/esphome/components/esp32_rmt_led_strip/light.py index 596770b96d..33ae44e435 100644 --- a/esphome/components/esp32_rmt_led_strip/light.py +++ b/esphome/components/esp32_rmt_led_strip/light.py @@ -3,7 +3,7 @@ import logging from esphome import pins import esphome.codegen as cg -from esphome.components import esp32, esp32_rmt, light +from esphome.components import esp32, light import esphome.config_validation as cv from esphome.const import ( CONF_CHIPSET, @@ -13,11 +13,9 @@ from esphome.const import ( CONF_OUTPUT_ID, CONF_PIN, CONF_RGB_ORDER, - CONF_RMT_CHANNEL, CONF_RMT_SYMBOLS, CONF_USE_DMA, ) -from esphome.core import CORE _LOGGER = logging.getLogger(__name__) @@ -69,53 +67,6 @@ CONF_RESET_HIGH = "reset_high" CONF_RESET_LOW = "reset_low" -class OptionalForIDF5(cv.SplitDefault): - @property - def default(self): - if not esp32_rmt.use_new_rmt_driver(): - return cv.UNDEFINED - return super().default - - @default.setter - def default(self, value): - # Ignore default set from vol.Optional - pass - - -def only_with_new_rmt_driver(obj): - if not esp32_rmt.use_new_rmt_driver(): - raise cv.Invalid( - "This feature is only available for the IDF framework version 5." - ) - return obj - - -def not_with_new_rmt_driver(obj): - if esp32_rmt.use_new_rmt_driver(): - raise cv.Invalid( - "This feature is not available for the IDF framework version 5." - ) - return obj - - -def final_validation(config): - if not esp32_rmt.use_new_rmt_driver(): - if CONF_RMT_CHANNEL not in config: - if CORE.using_esp_idf: - raise cv.Invalid( - "rmt_channel is a required option for IDF version < 5." - ) - raise cv.Invalid( - "rmt_channel is a required option for the Arduino framework." - ) - _LOGGER.warning( - "RMT_LED_STRIP support for IDF version < 5 is deprecated and will be removed soon." - ) - - -FINAL_VALIDATE_SCHEMA = final_validation - - CONFIG_SCHEMA = cv.All( light.ADDRESSABLE_LIGHT_SCHEMA.extend( { @@ -123,20 +74,17 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_PIN): pins.internal_gpio_output_pin_number, cv.Required(CONF_NUM_LEDS): cv.positive_not_null_int, cv.Required(CONF_RGB_ORDER): cv.enum(RGB_ORDERS, upper=True), - cv.Optional(CONF_RMT_CHANNEL): cv.All( - not_with_new_rmt_driver, esp32_rmt.validate_rmt_channel(tx=True) - ), - OptionalForIDF5( + cv.SplitDefault( CONF_RMT_SYMBOLS, - esp32_idf=192, - esp32_s2_idf=192, - esp32_s3_idf=192, - esp32_p4_idf=192, - esp32_c3_idf=96, - esp32_c5_idf=96, - esp32_c6_idf=96, - esp32_h2_idf=96, - ): cv.All(only_with_new_rmt_driver, cv.int_range(min=2)), + esp32=192, + esp32_s2=192, + esp32_s3=192, + esp32_p4=192, + esp32_c3=96, + esp32_c5=96, + esp32_c6=96, + esp32_h2=96, + ): cv.int_range(min=2), cv.Optional(CONF_MAX_REFRESH_RATE): cv.positive_time_period_microseconds, cv.Optional(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True), cv.Optional(CONF_IS_RGBW, default=False): cv.boolean, @@ -145,7 +93,6 @@ CONFIG_SCHEMA = cv.All( esp32.only_on_variant( supported=[esp32.const.VARIANT_ESP32S3, esp32.const.VARIANT_ESP32P4] ), - cv.only_with_esp_idf, cv.boolean, ), cv.Optional(CONF_USE_PSRAM, default=True): cv.boolean, @@ -218,15 +165,6 @@ async def to_code(config): cg.add(var.set_is_rgbw(config[CONF_IS_RGBW])) cg.add(var.set_is_wrgb(config[CONF_IS_WRGB])) cg.add(var.set_use_psram(config[CONF_USE_PSRAM])) - - if esp32_rmt.use_new_rmt_driver(): - cg.add(var.set_rmt_symbols(config[CONF_RMT_SYMBOLS])) - if CONF_USE_DMA in config: - cg.add(var.set_use_dma(config[CONF_USE_DMA])) - else: - rmt_channel_t = cg.global_ns.enum("rmt_channel_t") - cg.add( - var.set_rmt_channel( - getattr(rmt_channel_t, f"RMT_CHANNEL_{config[CONF_RMT_CHANNEL]}") - ) - ) + cg.add(var.set_rmt_symbols(config[CONF_RMT_SYMBOLS])) + if CONF_USE_DMA in config: + cg.add(var.set_use_dma(config[CONF_USE_DMA])) diff --git a/esphome/components/esp32_touch/esp32_touch.cpp b/esphome/components/esp32_touch/esp32_touch.cpp deleted file mode 100644 index 366aa10697..0000000000 --- a/esphome/components/esp32_touch/esp32_touch.cpp +++ /dev/null @@ -1,355 +0,0 @@ -#ifdef USE_ESP32 - -#include "esp32_touch.h" -#include "esphome/core/application.h" -#include "esphome/core/log.h" -#include "esphome/core/hal.h" - -#include - -namespace esphome { -namespace esp32_touch { - -static const char *const TAG = "esp32_touch"; - -void ESP32TouchComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - touch_pad_init(); -// set up and enable/start filtering based on ESP32 variant -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) - if (this->filter_configured_()) { - touch_filter_config_t filter_info = { - .mode = this->filter_mode_, - .debounce_cnt = this->debounce_count_, - .noise_thr = this->noise_threshold_, - .jitter_step = this->jitter_step_, - .smh_lvl = this->smooth_level_, - }; - touch_pad_filter_set_config(&filter_info); - touch_pad_filter_enable(); - } - - if (this->denoise_configured_()) { - touch_pad_denoise_t denoise = { - .grade = this->grade_, - .cap_level = this->cap_level_, - }; - touch_pad_denoise_set_config(&denoise); - touch_pad_denoise_enable(); - } - - if (this->waterproof_configured_()) { - touch_pad_waterproof_t waterproof = { - .guard_ring_pad = this->waterproof_guard_ring_pad_, - .shield_driver = this->waterproof_shield_driver_, - }; - touch_pad_waterproof_set_config(&waterproof); - touch_pad_waterproof_enable(); - } -#else - if (this->iir_filter_enabled_()) { - touch_pad_filter_start(this->iir_filter_); - } -#endif - -#if ESP_IDF_VERSION_MAJOR >= 5 && defined(USE_ESP32_VARIANT_ESP32) - touch_pad_set_measurement_clock_cycles(this->meas_cycle_); - touch_pad_set_measurement_interval(this->sleep_cycle_); -#else - touch_pad_set_meas_time(this->sleep_cycle_, this->meas_cycle_); -#endif - touch_pad_set_voltage(this->high_voltage_reference_, this->low_voltage_reference_, this->voltage_attenuation_); - - for (auto *child : this->children_) { -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) - touch_pad_config(child->get_touch_pad()); -#else - // Disable interrupt threshold - touch_pad_config(child->get_touch_pad(), 0); -#endif - } -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) - touch_pad_set_fsm_mode(TOUCH_FSM_MODE_TIMER); - touch_pad_fsm_start(); -#endif -} - -void ESP32TouchComponent::dump_config() { - ESP_LOGCONFIG(TAG, - "Config for ESP32 Touch Hub:\n" - " Meas cycle: %.2fms\n" - " Sleep cycle: %.2fms", - this->meas_cycle_ / (8000000.0f / 1000.0f), this->sleep_cycle_ / (150000.0f / 1000.0f)); - - const char *lv_s; - switch (this->low_voltage_reference_) { - case TOUCH_LVOLT_0V5: - lv_s = "0.5V"; - break; - case TOUCH_LVOLT_0V6: - lv_s = "0.6V"; - break; - case TOUCH_LVOLT_0V7: - lv_s = "0.7V"; - break; - case TOUCH_LVOLT_0V8: - lv_s = "0.8V"; - break; - default: - lv_s = "UNKNOWN"; - break; - } - ESP_LOGCONFIG(TAG, " Low Voltage Reference: %s", lv_s); - - const char *hv_s; - switch (this->high_voltage_reference_) { - case TOUCH_HVOLT_2V4: - hv_s = "2.4V"; - break; - case TOUCH_HVOLT_2V5: - hv_s = "2.5V"; - break; - case TOUCH_HVOLT_2V6: - hv_s = "2.6V"; - break; - case TOUCH_HVOLT_2V7: - hv_s = "2.7V"; - break; - default: - hv_s = "UNKNOWN"; - break; - } - ESP_LOGCONFIG(TAG, " High Voltage Reference: %s", hv_s); - - const char *atten_s; - switch (this->voltage_attenuation_) { - case TOUCH_HVOLT_ATTEN_1V5: - atten_s = "1.5V"; - break; - case TOUCH_HVOLT_ATTEN_1V: - atten_s = "1V"; - break; - case TOUCH_HVOLT_ATTEN_0V5: - atten_s = "0.5V"; - break; - case TOUCH_HVOLT_ATTEN_0V: - atten_s = "0V"; - break; - default: - atten_s = "UNKNOWN"; - break; - } - ESP_LOGCONFIG(TAG, " Voltage Attenuation: %s", atten_s); - -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) - if (this->filter_configured_()) { - const char *filter_mode_s; - switch (this->filter_mode_) { - case TOUCH_PAD_FILTER_IIR_4: - filter_mode_s = "IIR_4"; - break; - case TOUCH_PAD_FILTER_IIR_8: - filter_mode_s = "IIR_8"; - break; - case TOUCH_PAD_FILTER_IIR_16: - filter_mode_s = "IIR_16"; - break; - case TOUCH_PAD_FILTER_IIR_32: - filter_mode_s = "IIR_32"; - break; - case TOUCH_PAD_FILTER_IIR_64: - filter_mode_s = "IIR_64"; - break; - case TOUCH_PAD_FILTER_IIR_128: - filter_mode_s = "IIR_128"; - break; - case TOUCH_PAD_FILTER_IIR_256: - filter_mode_s = "IIR_256"; - break; - case TOUCH_PAD_FILTER_JITTER: - filter_mode_s = "JITTER"; - break; - default: - filter_mode_s = "UNKNOWN"; - break; - } - ESP_LOGCONFIG(TAG, - " Filter mode: %s\n" - " Debounce count: %" PRIu32 "\n" - " Noise threshold coefficient: %" PRIu32 "\n" - " Jitter filter step size: %" PRIu32, - filter_mode_s, this->debounce_count_, this->noise_threshold_, this->jitter_step_); - const char *smooth_level_s; - switch (this->smooth_level_) { - case TOUCH_PAD_SMOOTH_OFF: - smooth_level_s = "OFF"; - break; - case TOUCH_PAD_SMOOTH_IIR_2: - smooth_level_s = "IIR_2"; - break; - case TOUCH_PAD_SMOOTH_IIR_4: - smooth_level_s = "IIR_4"; - break; - case TOUCH_PAD_SMOOTH_IIR_8: - smooth_level_s = "IIR_8"; - break; - default: - smooth_level_s = "UNKNOWN"; - break; - } - ESP_LOGCONFIG(TAG, " Smooth level: %s", smooth_level_s); - } - - if (this->denoise_configured_()) { - const char *grade_s; - switch (this->grade_) { - case TOUCH_PAD_DENOISE_BIT12: - grade_s = "BIT12"; - break; - case TOUCH_PAD_DENOISE_BIT10: - grade_s = "BIT10"; - break; - case TOUCH_PAD_DENOISE_BIT8: - grade_s = "BIT8"; - break; - case TOUCH_PAD_DENOISE_BIT4: - grade_s = "BIT4"; - break; - default: - grade_s = "UNKNOWN"; - break; - } - ESP_LOGCONFIG(TAG, " Denoise grade: %s", grade_s); - - const char *cap_level_s; - switch (this->cap_level_) { - case TOUCH_PAD_DENOISE_CAP_L0: - cap_level_s = "L0"; - break; - case TOUCH_PAD_DENOISE_CAP_L1: - cap_level_s = "L1"; - break; - case TOUCH_PAD_DENOISE_CAP_L2: - cap_level_s = "L2"; - break; - case TOUCH_PAD_DENOISE_CAP_L3: - cap_level_s = "L3"; - break; - case TOUCH_PAD_DENOISE_CAP_L4: - cap_level_s = "L4"; - break; - case TOUCH_PAD_DENOISE_CAP_L5: - cap_level_s = "L5"; - break; - case TOUCH_PAD_DENOISE_CAP_L6: - cap_level_s = "L6"; - break; - case TOUCH_PAD_DENOISE_CAP_L7: - cap_level_s = "L7"; - break; - default: - cap_level_s = "UNKNOWN"; - break; - } - ESP_LOGCONFIG(TAG, " Denoise capacitance level: %s", cap_level_s); - } -#else - if (this->iir_filter_enabled_()) { - ESP_LOGCONFIG(TAG, " IIR Filter: %" PRIu32 "ms", this->iir_filter_); - } else { - ESP_LOGCONFIG(TAG, " IIR Filter DISABLED"); - } -#endif - - if (this->setup_mode_) { - ESP_LOGCONFIG(TAG, " Setup Mode ENABLED"); - } - - for (auto *child : this->children_) { - LOG_BINARY_SENSOR(" ", "Touch Pad", child); - ESP_LOGCONFIG(TAG, " Pad: T%" PRIu32, (uint32_t) child->get_touch_pad()); - ESP_LOGCONFIG(TAG, " Threshold: %" PRIu32, child->get_threshold()); - } -} - -uint32_t ESP32TouchComponent::component_touch_pad_read(touch_pad_t tp) { -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) - uint32_t value = 0; - if (this->filter_configured_()) { - touch_pad_filter_read_smooth(tp, &value); - } else { - touch_pad_read_raw_data(tp, &value); - } -#else - uint16_t value = 0; - if (this->iir_filter_enabled_()) { - touch_pad_read_filtered(tp, &value); - } else { - touch_pad_read(tp, &value); - } -#endif - return value; -} - -void ESP32TouchComponent::loop() { - const uint32_t now = App.get_loop_component_start_time(); - bool should_print = this->setup_mode_ && now - this->setup_mode_last_log_print_ > 250; - for (auto *child : this->children_) { - child->value_ = this->component_touch_pad_read(child->get_touch_pad()); -#if !(defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)) - child->publish_state(child->value_ < child->get_threshold()); -#else - child->publish_state(child->value_ > child->get_threshold()); -#endif - - if (should_print) { - ESP_LOGD(TAG, "Touch Pad '%s' (T%" PRIu32 "): %" PRIu32, child->get_name().c_str(), - (uint32_t) child->get_touch_pad(), child->value_); - } - - App.feed_wdt(); - } - - if (should_print) { - // Avoid spamming logs - this->setup_mode_last_log_print_ = now; - } -} - -void ESP32TouchComponent::on_shutdown() { - bool is_wakeup_source = false; - -#if !(defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)) - if (this->iir_filter_enabled_()) { - touch_pad_filter_stop(); - touch_pad_filter_delete(); - } -#endif - - for (auto *child : this->children_) { - if (child->get_wakeup_threshold() != 0) { - if (!is_wakeup_source) { - is_wakeup_source = true; - // Touch sensor FSM mode must be 'TOUCH_FSM_MODE_TIMER' to use it to wake-up. - touch_pad_set_fsm_mode(TOUCH_FSM_MODE_TIMER); - } - -#if !(defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)) - // No filter available when using as wake-up source. - touch_pad_config(child->get_touch_pad(), child->get_wakeup_threshold()); -#endif - } - } - - if (!is_wakeup_source) { - touch_pad_deinit(); - } -} - -ESP32TouchBinarySensor::ESP32TouchBinarySensor(touch_pad_t touch_pad, uint32_t threshold, uint32_t wakeup_threshold) - : touch_pad_(touch_pad), threshold_(threshold), wakeup_threshold_(wakeup_threshold) {} - -} // namespace esp32_touch -} // namespace esphome - -#endif diff --git a/esphome/components/esp32_touch/esp32_touch.h b/esphome/components/esp32_touch/esp32_touch.h index 0eac590ce7..5a91b1c750 100644 --- a/esphome/components/esp32_touch/esp32_touch.h +++ b/esphome/components/esp32_touch/esp32_touch.h @@ -9,10 +9,26 @@ #include #include +#include +#include namespace esphome { namespace esp32_touch { +// IMPORTANT: Touch detection logic differs between ESP32 variants: +// - ESP32 v1 (original): Touch detected when value < threshold (capacitance increase causes value decrease) +// - ESP32-S2/S3 v2: Touch detected when value > threshold (capacitance increase causes value increase) +// This inversion is due to different hardware implementations between chip generations. +// +// INTERRUPT BEHAVIOR: +// - ESP32 v1: Interrupts fire when ANY pad is touched and continue while touched. +// Releases are detected by timeout since hardware doesn't generate release interrupts. +// - ESP32-S2/S3 v2: Hardware supports both touch and release interrupts, but release +// interrupts are unreliable and sometimes don't fire. We now only use touch interrupts +// and detect releases via timeout, similar to v1. + +static const uint32_t SETUP_MODE_LOG_INTERVAL_MS = 250; + class ESP32TouchBinarySensor; class ESP32TouchComponent : public Component { @@ -31,6 +47,14 @@ class ESP32TouchComponent : public Component { void set_voltage_attenuation(touch_volt_atten_t voltage_attenuation) { this->voltage_attenuation_ = voltage_attenuation; } + + void setup() override; + void dump_config() override; + void loop() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + void on_shutdown() override; + #if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) void set_filter_mode(touch_filter_mode_t filter_mode) { this->filter_mode_ = filter_mode; } void set_debounce_count(uint32_t debounce_count) { this->debounce_count_ = debounce_count; } @@ -47,17 +71,90 @@ class ESP32TouchComponent : public Component { void set_iir_filter(uint32_t iir_filter) { this->iir_filter_ = iir_filter; } #endif - uint32_t component_touch_pad_read(touch_pad_t tp); + protected: + // Common helper methods + void dump_config_base_(); + void dump_config_sensors_(); + bool create_touch_queue_(); + void cleanup_touch_queue_(); + void configure_wakeup_pads_(); - void setup() override; - void dump_config() override; - void loop() override; - float get_setup_priority() const override { return setup_priority::DATA; } + // Helper methods for loop() logic + void process_setup_mode_logging_(uint32_t now); + bool should_check_for_releases_(uint32_t now); + void publish_initial_state_if_needed_(ESP32TouchBinarySensor *child, uint32_t now); + void check_and_disable_loop_if_all_released_(size_t pads_off); + void calculate_release_timeout_(); - void on_shutdown() override; + // Common members + std::vector children_; + bool setup_mode_{false}; + uint32_t setup_mode_last_log_print_{0}; + uint32_t last_release_check_{0}; + uint32_t release_timeout_ms_{1500}; + uint32_t release_check_interval_ms_{50}; + + // Common configuration parameters + uint16_t sleep_cycle_{4095}; + uint16_t meas_cycle_{65535}; + touch_low_volt_t low_voltage_reference_{TOUCH_LVOLT_0V5}; + touch_high_volt_t high_voltage_reference_{TOUCH_HVOLT_2V7}; + touch_volt_atten_t voltage_attenuation_{TOUCH_HVOLT_ATTEN_0V}; + + // Common constants + static constexpr uint32_t MINIMUM_RELEASE_TIME_MS = 100; + + // ==================== PLATFORM SPECIFIC ==================== + +#ifdef USE_ESP32_VARIANT_ESP32 + // ESP32 v1 specific + + static void touch_isr_handler(void *arg); + QueueHandle_t touch_queue_{nullptr}; + + private: + // Touch event structure for ESP32 v1 + // Contains touch pad info, value, and touch state for queue communication + struct TouchPadEventV1 { + touch_pad_t pad; + uint32_t value; + bool is_touched; + }; protected: -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) + uint32_t iir_filter_{0}; + + bool iir_filter_enabled_() const { return this->iir_filter_ > 0; } + +#elif defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) + // ESP32-S2/S3 v2 specific + static void touch_isr_handler(void *arg); + QueueHandle_t touch_queue_{nullptr}; + + private: + // Touch event structure for ESP32 v2 (S2/S3) + // Contains touch pad and interrupt mask for queue communication + struct TouchPadEventV2 { + touch_pad_t pad; + uint32_t intr_mask; + }; + + protected: + // Filter configuration + touch_filter_mode_t filter_mode_{TOUCH_PAD_FILTER_MAX}; + uint32_t debounce_count_{0}; + uint32_t noise_threshold_{0}; + uint32_t jitter_step_{0}; + touch_smooth_mode_t smooth_level_{TOUCH_PAD_SMOOTH_MAX}; + + // Denoise configuration + touch_pad_denoise_grade_t grade_{TOUCH_PAD_DENOISE_MAX}; + touch_pad_denoise_cap_t cap_level_{TOUCH_PAD_DENOISE_CAP_MAX}; + + // Waterproof configuration + touch_pad_t waterproof_guard_ring_pad_{TOUCH_PAD_MAX}; + touch_pad_shield_driver_t waterproof_shield_driver_{TOUCH_PAD_SHIELD_DRV_MAX}; + bool filter_configured_() const { return (this->filter_mode_ != TOUCH_PAD_FILTER_MAX) && (this->smooth_level_ != TOUCH_PAD_SMOOTH_MAX); } @@ -68,43 +165,78 @@ class ESP32TouchComponent : public Component { return (this->waterproof_guard_ring_pad_ != TOUCH_PAD_MAX) && (this->waterproof_shield_driver_ != TOUCH_PAD_SHIELD_DRV_MAX); } -#else - bool iir_filter_enabled_() const { return this->iir_filter_ > 0; } + + // Helper method to read touch values - non-blocking operation + // Returns the current touch pad value using either filtered or raw reading + // based on the filter configuration + uint32_t read_touch_value(touch_pad_t pad) const; + + // Helper to update touch state with a known state + void update_touch_state_(ESP32TouchBinarySensor *child, bool is_touched); + + // Helper to read touch value and update state for a given child + bool check_and_update_touch_state_(ESP32TouchBinarySensor *child); #endif - std::vector children_; - bool setup_mode_{false}; - uint32_t setup_mode_last_log_print_{0}; - // common parameters - uint16_t sleep_cycle_{4095}; - uint16_t meas_cycle_{65535}; - touch_low_volt_t low_voltage_reference_{TOUCH_LVOLT_0V5}; - touch_high_volt_t high_voltage_reference_{TOUCH_HVOLT_2V7}; - touch_volt_atten_t voltage_attenuation_{TOUCH_HVOLT_ATTEN_0V}; -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) - touch_filter_mode_t filter_mode_{TOUCH_PAD_FILTER_MAX}; - uint32_t debounce_count_{0}; - uint32_t noise_threshold_{0}; - uint32_t jitter_step_{0}; - touch_smooth_mode_t smooth_level_{TOUCH_PAD_SMOOTH_MAX}; - touch_pad_denoise_grade_t grade_{TOUCH_PAD_DENOISE_MAX}; - touch_pad_denoise_cap_t cap_level_{TOUCH_PAD_DENOISE_CAP_MAX}; - touch_pad_t waterproof_guard_ring_pad_{TOUCH_PAD_MAX}; - touch_pad_shield_driver_t waterproof_shield_driver_{TOUCH_PAD_SHIELD_DRV_MAX}; -#else - uint32_t iir_filter_{0}; -#endif + // Helper functions for dump_config - common to both implementations + static const char *get_low_voltage_reference_str(touch_low_volt_t ref) { + switch (ref) { + case TOUCH_LVOLT_0V5: + return "0.5V"; + case TOUCH_LVOLT_0V6: + return "0.6V"; + case TOUCH_LVOLT_0V7: + return "0.7V"; + case TOUCH_LVOLT_0V8: + return "0.8V"; + default: + return "UNKNOWN"; + } + } + + static const char *get_high_voltage_reference_str(touch_high_volt_t ref) { + switch (ref) { + case TOUCH_HVOLT_2V4: + return "2.4V"; + case TOUCH_HVOLT_2V5: + return "2.5V"; + case TOUCH_HVOLT_2V6: + return "2.6V"; + case TOUCH_HVOLT_2V7: + return "2.7V"; + default: + return "UNKNOWN"; + } + } + + static const char *get_voltage_attenuation_str(touch_volt_atten_t atten) { + switch (atten) { + case TOUCH_HVOLT_ATTEN_1V5: + return "1.5V"; + case TOUCH_HVOLT_ATTEN_1V: + return "1V"; + case TOUCH_HVOLT_ATTEN_0V5: + return "0.5V"; + case TOUCH_HVOLT_ATTEN_0V: + return "0V"; + default: + return "UNKNOWN"; + } + } }; /// Simple helper class to expose a touch pad value as a binary sensor. class ESP32TouchBinarySensor : public binary_sensor::BinarySensor { public: - ESP32TouchBinarySensor(touch_pad_t touch_pad, uint32_t threshold, uint32_t wakeup_threshold); + ESP32TouchBinarySensor(touch_pad_t touch_pad, uint32_t threshold, uint32_t wakeup_threshold) + : touch_pad_(touch_pad), threshold_(threshold), wakeup_threshold_(wakeup_threshold) {} touch_pad_t get_touch_pad() const { return this->touch_pad_; } uint32_t get_threshold() const { return this->threshold_; } void set_threshold(uint32_t threshold) { this->threshold_ = threshold; } +#ifdef USE_ESP32_VARIANT_ESP32 uint32_t get_value() const { return this->value_; } +#endif uint32_t get_wakeup_threshold() const { return this->wakeup_threshold_; } protected: @@ -112,8 +244,22 @@ class ESP32TouchBinarySensor : public binary_sensor::BinarySensor { touch_pad_t touch_pad_{TOUCH_PAD_MAX}; uint32_t threshold_{0}; + uint32_t benchmark_{}; +#ifdef USE_ESP32_VARIANT_ESP32 uint32_t value_{0}; +#endif + bool last_state_{false}; const uint32_t wakeup_threshold_{0}; + + // Track last touch time for timeout-based release detection + // Design note: last_touch_time_ does not require synchronization primitives because: + // 1. ESP32 guarantees atomic 32-bit aligned reads/writes + // 2. ISR only writes timestamps, main loop only reads + // 3. Timing tolerance allows for occasional stale reads (50ms check interval) + // 4. Queue operations provide implicit memory barriers + // Using atomic/critical sections would add overhead without meaningful benefit + uint32_t last_touch_time_{}; + bool initial_state_published_{}; }; } // namespace esp32_touch diff --git a/esphome/components/esp32_touch/esp32_touch_common.cpp b/esphome/components/esp32_touch/esp32_touch_common.cpp new file mode 100644 index 0000000000..2d93de077e --- /dev/null +++ b/esphome/components/esp32_touch/esp32_touch_common.cpp @@ -0,0 +1,162 @@ +#ifdef USE_ESP32 + +#include "esp32_touch.h" +#include "esphome/core/log.h" +#include + +#include "soc/rtc.h" + +namespace esphome { +namespace esp32_touch { + +static const char *const TAG = "esp32_touch"; + +void ESP32TouchComponent::dump_config_base_() { + const char *lv_s = get_low_voltage_reference_str(this->low_voltage_reference_); + const char *hv_s = get_high_voltage_reference_str(this->high_voltage_reference_); + const char *atten_s = get_voltage_attenuation_str(this->voltage_attenuation_); + + ESP_LOGCONFIG(TAG, + "Config for ESP32 Touch Hub:\n" + " Meas cycle: %.2fms\n" + " Sleep cycle: %.2fms\n" + " Low Voltage Reference: %s\n" + " High Voltage Reference: %s\n" + " Voltage Attenuation: %s\n" + " Release Timeout: %" PRIu32 "ms\n", + this->meas_cycle_ / (8000000.0f / 1000.0f), this->sleep_cycle_ / (150000.0f / 1000.0f), lv_s, hv_s, + atten_s, this->release_timeout_ms_); +} + +void ESP32TouchComponent::dump_config_sensors_() { + for (auto *child : this->children_) { + LOG_BINARY_SENSOR(" ", "Touch Pad", child); + ESP_LOGCONFIG(TAG, + " Pad: T%u\n" + " Threshold: %" PRIu32 "\n" + " Benchmark: %" PRIu32, + (unsigned) child->touch_pad_, child->threshold_, child->benchmark_); + } +} + +bool ESP32TouchComponent::create_touch_queue_() { + // Queue size calculation: children * 4 allows for burst scenarios where ISR + // fires multiple times before main loop processes. + size_t queue_size = this->children_.size() * 4; + if (queue_size < 8) + queue_size = 8; + +#ifdef USE_ESP32_VARIANT_ESP32 + this->touch_queue_ = xQueueCreate(queue_size, sizeof(TouchPadEventV1)); +#else + this->touch_queue_ = xQueueCreate(queue_size, sizeof(TouchPadEventV2)); +#endif + + if (this->touch_queue_ == nullptr) { + ESP_LOGE(TAG, "Failed to create touch event queue of size %" PRIu32, (uint32_t) queue_size); + this->mark_failed(); + return false; + } + return true; +} + +void ESP32TouchComponent::cleanup_touch_queue_() { + if (this->touch_queue_) { + vQueueDelete(this->touch_queue_); + this->touch_queue_ = nullptr; + } +} + +void ESP32TouchComponent::configure_wakeup_pads_() { + bool is_wakeup_source = false; + + // Check if any pad is configured for wakeup + for (auto *child : this->children_) { + if (child->get_wakeup_threshold() != 0) { + is_wakeup_source = true; + +#ifdef USE_ESP32_VARIANT_ESP32 + // ESP32 v1: No filter available when using as wake-up source. + touch_pad_config(child->get_touch_pad(), child->get_wakeup_threshold()); +#else + // ESP32-S2/S3 v2: Set threshold for wakeup + touch_pad_set_thresh(child->get_touch_pad(), child->get_wakeup_threshold()); +#endif + } + } + + if (!is_wakeup_source) { + // If no pad is configured for wakeup, deinitialize touch pad + touch_pad_deinit(); + } +} + +void ESP32TouchComponent::process_setup_mode_logging_(uint32_t now) { + if (this->setup_mode_ && now - this->setup_mode_last_log_print_ > SETUP_MODE_LOG_INTERVAL_MS) { + for (auto *child : this->children_) { +#ifdef USE_ESP32_VARIANT_ESP32 + ESP_LOGD(TAG, "Touch Pad '%s' (T%" PRIu32 "): %" PRIu32, child->get_name().c_str(), + (uint32_t) child->get_touch_pad(), child->value_); +#else + // Read the value being used for touch detection + uint32_t value = this->read_touch_value(child->get_touch_pad()); + ESP_LOGD(TAG, "Touch Pad '%s' (T%d): %d", child->get_name().c_str(), child->get_touch_pad(), value); +#endif + } + this->setup_mode_last_log_print_ = now; + } +} + +bool ESP32TouchComponent::should_check_for_releases_(uint32_t now) { + if (now - this->last_release_check_ < this->release_check_interval_ms_) { + return false; + } + this->last_release_check_ = now; + return true; +} + +void ESP32TouchComponent::publish_initial_state_if_needed_(ESP32TouchBinarySensor *child, uint32_t now) { + if (!child->initial_state_published_) { + // Check if enough time has passed since startup + if (now > this->release_timeout_ms_) { + child->publish_initial_state(false); + child->initial_state_published_ = true; + ESP_LOGV(TAG, "Touch Pad '%s' state: OFF (initial)", child->get_name().c_str()); + } + } +} + +void ESP32TouchComponent::check_and_disable_loop_if_all_released_(size_t pads_off) { + // Disable the loop to save CPU cycles when all pads are off and not in setup mode. + if (pads_off == this->children_.size() && !this->setup_mode_) { + this->disable_loop(); + } +} + +void ESP32TouchComponent::calculate_release_timeout_() { + // Calculate release timeout based on sleep cycle + // Design note: Hardware limitation - interrupts only fire reliably on touch (not release) + // We must use timeout-based detection for release events + // Formula: 3 sleep cycles converted to ms, with MINIMUM_RELEASE_TIME_MS minimum + // Per ESP-IDF docs: t_sleep = sleep_cycle / SOC_CLK_RC_SLOW_FREQ_APPROX + + uint32_t rtc_freq = rtc_clk_slow_freq_get_hz(); + + // Calculate timeout as 3 sleep cycles + this->release_timeout_ms_ = (this->sleep_cycle_ * 1000 * 3) / rtc_freq; + + if (this->release_timeout_ms_ < MINIMUM_RELEASE_TIME_MS) { + this->release_timeout_ms_ = MINIMUM_RELEASE_TIME_MS; + } + + // Check for releases at 1/4 the timeout interval + // Since hardware doesn't generate reliable release interrupts, we must poll + // for releases in the main loop. Checking at 1/4 the timeout interval provides + // a good balance between responsiveness and efficiency. + this->release_check_interval_ms_ = this->release_timeout_ms_ / 4; +} + +} // namespace esp32_touch +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/esp32_touch/esp32_touch_v1.cpp b/esphome/components/esp32_touch/esp32_touch_v1.cpp new file mode 100644 index 0000000000..6f05610ed6 --- /dev/null +++ b/esphome/components/esp32_touch/esp32_touch_v1.cpp @@ -0,0 +1,238 @@ +#ifdef USE_ESP32_VARIANT_ESP32 + +#include "esp32_touch.h" +#include "esphome/core/application.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +#include +#include + +// Include HAL for ISR-safe touch reading +#include "hal/touch_sensor_ll.h" + +namespace esphome { +namespace esp32_touch { + +static const char *const TAG = "esp32_touch"; + +void ESP32TouchComponent::setup() { + // Create queue for touch events + // Queue size calculation: children * 4 allows for burst scenarios where ISR + // fires multiple times before main loop processes. This is important because + // ESP32 v1 scans all pads on each interrupt, potentially sending multiple events. + if (!this->create_touch_queue_()) { + return; + } + + touch_pad_init(); + touch_pad_set_fsm_mode(TOUCH_FSM_MODE_TIMER); + + // Set up IIR filter if enabled + if (this->iir_filter_enabled_()) { + touch_pad_filter_start(this->iir_filter_); + } + + // Configure measurement parameters +#if ESP_IDF_VERSION_MAJOR >= 5 + touch_pad_set_measurement_clock_cycles(this->meas_cycle_); + touch_pad_set_measurement_interval(this->sleep_cycle_); +#else + touch_pad_set_meas_time(this->sleep_cycle_, this->meas_cycle_); +#endif + touch_pad_set_voltage(this->high_voltage_reference_, this->low_voltage_reference_, this->voltage_attenuation_); + + // Configure each touch pad + for (auto *child : this->children_) { + touch_pad_config(child->get_touch_pad(), child->get_threshold()); + } + + // Register ISR handler + esp_err_t err = touch_pad_isr_register(touch_isr_handler, this); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to register touch ISR: %s", esp_err_to_name(err)); + this->cleanup_touch_queue_(); + this->mark_failed(); + return; + } + + // Calculate release timeout based on sleep cycle + this->calculate_release_timeout_(); + + // Enable touch pad interrupt + touch_pad_intr_enable(); +} + +void ESP32TouchComponent::dump_config() { + this->dump_config_base_(); + + if (this->iir_filter_enabled_()) { + ESP_LOGCONFIG(TAG, " IIR Filter: %" PRIu32 "ms", this->iir_filter_); + } else { + ESP_LOGCONFIG(TAG, " IIR Filter DISABLED"); + } + + if (this->setup_mode_) { + ESP_LOGCONFIG(TAG, " Setup Mode ENABLED"); + } + + this->dump_config_sensors_(); +} + +void ESP32TouchComponent::loop() { + const uint32_t now = App.get_loop_component_start_time(); + + // Print debug info for all pads in setup mode + this->process_setup_mode_logging_(now); + + // Process any queued touch events from interrupts + // Note: Events are only sent by ISR for pads that were measured in that cycle (value != 0) + // This is more efficient than sending all pad states every interrupt + TouchPadEventV1 event; + while (xQueueReceive(this->touch_queue_, &event, 0) == pdTRUE) { + // Find the corresponding sensor - O(n) search is acceptable since events are infrequent + for (auto *child : this->children_) { + if (child->get_touch_pad() != event.pad) { + continue; + } + + // Found matching pad - process it + child->value_ = event.value; + + // The interrupt gives us the touch state directly + bool new_state = event.is_touched; + + // Track when we last saw this pad as touched + if (new_state) { + child->last_touch_time_ = now; + } + + // Only publish if state changed - this filters out repeated events + if (new_state != child->last_state_) { + child->last_state_ = new_state; + child->publish_state(new_state); + // Original ESP32: ISR only fires when touched, release is detected by timeout + // Note: ESP32 v1 uses inverted logic - touched when value < threshold + ESP_LOGV(TAG, "Touch Pad '%s' state: ON (value: %" PRIu32 " < threshold: %" PRIu32 ")", + child->get_name().c_str(), event.value, child->get_threshold()); + } + break; // Exit inner loop after processing matching pad + } + } + + // Check for released pads periodically + if (!this->should_check_for_releases_(now)) { + return; + } + + size_t pads_off = 0; + for (auto *child : this->children_) { + // Handle initial state publication after startup + this->publish_initial_state_if_needed_(child, now); + + if (child->last_state_) { + // Pad is currently in touched state - check for release timeout + // Using subtraction handles 32-bit rollover correctly + uint32_t time_diff = now - child->last_touch_time_; + + // Check if we haven't seen this pad recently + if (time_diff > this->release_timeout_ms_) { + // Haven't seen this pad recently, assume it's released + child->last_state_ = false; + child->publish_state(false); + ESP_LOGV(TAG, "Touch Pad '%s' state: OFF (timeout)", child->get_name().c_str()); + pads_off++; + } + } else { + // Pad is already off + pads_off++; + } + } + + // Disable the loop to save CPU cycles when all pads are off and not in setup mode. + // The loop will be re-enabled by the ISR when any touch pad is touched. + // v1 hardware limitations require us to check all pads are off because: + // - v1 only generates interrupts on touch events (not releases) + // - We must poll for release timeouts in the main loop + // - We can only safely disable when no pads need timeout monitoring + this->check_and_disable_loop_if_all_released_(pads_off); +} + +void ESP32TouchComponent::on_shutdown() { + touch_pad_intr_disable(); + touch_pad_isr_deregister(touch_isr_handler, this); + this->cleanup_touch_queue_(); + + if (this->iir_filter_enabled_()) { + touch_pad_filter_stop(); + touch_pad_filter_delete(); + } + + // Configure wakeup pads if any are set + this->configure_wakeup_pads_(); +} + +void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) { + ESP32TouchComponent *component = static_cast(arg); + + touch_pad_clear_status(); + + // INTERRUPT BEHAVIOR: On ESP32 v1 hardware, the interrupt fires when ANY configured + // touch pad detects a touch (value goes below threshold). The hardware does NOT + // generate interrupts on release - only on touch events. + // The interrupt will continue to fire periodically (based on sleep_cycle) as long + // 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. + + // Process all configured pads to check their current state + // 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 + for (auto *child : component->children_) { + touch_pad_t pad = child->get_touch_pad(); + + // Read current value using ISR-safe API + uint32_t value; + if (component->iir_filter_enabled_()) { + uint16_t temp_value = 0; + touch_pad_read_filtered(pad, &temp_value); + value = temp_value; + } else { + // Use low-level HAL function when filter is not enabled + value = touch_ll_read_raw_data(pad); + } + + // Skip pads with 0 value - they haven't been measured in this cycle + // This is important: not all pads are measured every interrupt cycle, + // only those that the hardware has updated + if (value == 0) { + 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 + // We send both touched and untouched states because the ISR doesn't + // track previous state (to keep ISR fast and simple) + TouchPadEventV1 event; + event.pad = pad; + event.value = value; + event.is_touched = is_touched; + + // Send to queue from ISR - non-blocking, drops if queue full + BaseType_t x_higher_priority_task_woken = pdFALSE; + xQueueSendFromISR(component->touch_queue_, &event, &x_higher_priority_task_woken); + component->enable_loop_soon_any_context(); + if (x_higher_priority_task_woken) { + portYIELD_FROM_ISR(); + } + } +} + +} // namespace esp32_touch +} // namespace esphome + +#endif // USE_ESP32_VARIANT_ESP32 diff --git a/esphome/components/esp32_touch/esp32_touch_v2.cpp b/esphome/components/esp32_touch/esp32_touch_v2.cpp new file mode 100644 index 0000000000..afd2655fd7 --- /dev/null +++ b/esphome/components/esp32_touch/esp32_touch_v2.cpp @@ -0,0 +1,397 @@ +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) + +#include "esp32_touch.h" +#include "esphome/core/application.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace esp32_touch { + +static const char *const TAG = "esp32_touch"; + +// Helper to update touch state with a known state +void ESP32TouchComponent::update_touch_state_(ESP32TouchBinarySensor *child, bool is_touched) { + // Always update timer when touched + if (is_touched) { + child->last_touch_time_ = App.get_loop_component_start_time(); + } + + if (child->last_state_ != is_touched) { + child->last_state_ = is_touched; + child->publish_state(is_touched); + if (is_touched) { + // ESP32-S2/S3 v2: touched when value > threshold + ESP_LOGV(TAG, "Touch Pad '%s' state: ON (value: %" PRIu32 " > threshold: %" PRIu32 ")", child->get_name().c_str(), + this->read_touch_value(child->touch_pad_), child->threshold_ + child->benchmark_); + } else { + ESP_LOGV(TAG, "Touch Pad '%s' state: OFF", child->get_name().c_str()); + } + } +} + +// Helper to read touch value and update state for a given child (used for timeout events) +bool ESP32TouchComponent::check_and_update_touch_state_(ESP32TouchBinarySensor *child) { + // Read current touch value + uint32_t value = this->read_touch_value(child->touch_pad_); + + // ESP32-S2/S3 v2: Touch is detected when value > threshold + benchmark + ESP_LOGV(TAG, + "Checking touch state for '%s' (T%d): value = %" PRIu32 ", threshold = %" PRIu32 ", benchmark = %" PRIu32, + child->get_name().c_str(), child->touch_pad_, value, child->threshold_, child->benchmark_); + bool is_touched = value > child->benchmark_ + child->threshold_; + + this->update_touch_state_(child, is_touched); + return is_touched; +} + +void ESP32TouchComponent::setup() { + // Create queue for touch events first + if (!this->create_touch_queue_()) { + return; + } + + // Initialize touch pad peripheral + esp_err_t init_err = touch_pad_init(); + if (init_err != ESP_OK) { + ESP_LOGE(TAG, "Failed to initialize touch pad: %s", esp_err_to_name(init_err)); + this->mark_failed(); + return; + } + + // Configure each touch pad first + for (auto *child : this->children_) { + esp_err_t config_err = touch_pad_config(child->touch_pad_); + if (config_err != ESP_OK) { + ESP_LOGE(TAG, "Failed to configure touch pad %d: %s", child->touch_pad_, esp_err_to_name(config_err)); + } + } + + // Set up filtering if configured + if (this->filter_configured_()) { + touch_filter_config_t filter_info = { + .mode = this->filter_mode_, + .debounce_cnt = this->debounce_count_, + .noise_thr = this->noise_threshold_, + .jitter_step = this->jitter_step_, + .smh_lvl = this->smooth_level_, + }; + touch_pad_filter_set_config(&filter_info); + touch_pad_filter_enable(); + } + + if (this->denoise_configured_()) { + touch_pad_denoise_t denoise = { + .grade = this->grade_, + .cap_level = this->cap_level_, + }; + touch_pad_denoise_set_config(&denoise); + touch_pad_denoise_enable(); + } + + if (this->waterproof_configured_()) { + touch_pad_waterproof_t waterproof = { + .guard_ring_pad = this->waterproof_guard_ring_pad_, + .shield_driver = this->waterproof_shield_driver_, + }; + touch_pad_waterproof_set_config(&waterproof); + touch_pad_waterproof_enable(); + } + + // Configure measurement parameters + touch_pad_set_voltage(this->high_voltage_reference_, this->low_voltage_reference_, this->voltage_attenuation_); + touch_pad_set_charge_discharge_times(this->meas_cycle_); + touch_pad_set_measurement_interval(this->sleep_cycle_); + + // Configure timeout if needed + touch_pad_timeout_set(true, TOUCH_PAD_THRESHOLD_MAX); + + // Register ISR handler with interrupt mask + esp_err_t err = + touch_pad_isr_register(touch_isr_handler, this, static_cast(TOUCH_PAD_INTR_MASK_ALL)); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to register touch ISR: %s", esp_err_to_name(err)); + this->cleanup_touch_queue_(); + this->mark_failed(); + return; + } + + // Set thresholds for each pad BEFORE starting FSM + for (auto *child : this->children_) { + if (child->threshold_ != 0) { + touch_pad_set_thresh(child->touch_pad_, child->threshold_); + } + } + + // Enable interrupts - only ACTIVE and TIMEOUT + // NOTE: We intentionally don't enable INACTIVE interrupts because they are unreliable + // on ESP32-S2/S3 hardware and sometimes don't fire. Instead, we use timeout-based + // release detection with the ability to verify the actual state. + touch_pad_intr_enable(static_cast(TOUCH_PAD_INTR_MASK_ACTIVE | TOUCH_PAD_INTR_MASK_TIMEOUT)); + + // Set FSM mode before starting + touch_pad_set_fsm_mode(TOUCH_FSM_MODE_TIMER); + + // Start FSM + touch_pad_fsm_start(); + + // Calculate release timeout based on sleep cycle + this->calculate_release_timeout_(); +} + +void ESP32TouchComponent::dump_config() { + this->dump_config_base_(); + + if (this->filter_configured_()) { + const char *filter_mode_s; + switch (this->filter_mode_) { + case TOUCH_PAD_FILTER_IIR_4: + filter_mode_s = "IIR_4"; + break; + case TOUCH_PAD_FILTER_IIR_8: + filter_mode_s = "IIR_8"; + break; + case TOUCH_PAD_FILTER_IIR_16: + filter_mode_s = "IIR_16"; + break; + case TOUCH_PAD_FILTER_IIR_32: + filter_mode_s = "IIR_32"; + break; + case TOUCH_PAD_FILTER_IIR_64: + filter_mode_s = "IIR_64"; + break; + case TOUCH_PAD_FILTER_IIR_128: + filter_mode_s = "IIR_128"; + break; + case TOUCH_PAD_FILTER_IIR_256: + filter_mode_s = "IIR_256"; + break; + case TOUCH_PAD_FILTER_JITTER: + filter_mode_s = "JITTER"; + break; + default: + filter_mode_s = "UNKNOWN"; + break; + } + ESP_LOGCONFIG(TAG, + " Filter mode: %s\n" + " Debounce count: %" PRIu32 "\n" + " Noise threshold coefficient: %" PRIu32 "\n" + " Jitter filter step size: %" PRIu32, + filter_mode_s, this->debounce_count_, this->noise_threshold_, this->jitter_step_); + const char *smooth_level_s; + switch (this->smooth_level_) { + case TOUCH_PAD_SMOOTH_OFF: + smooth_level_s = "OFF"; + break; + case TOUCH_PAD_SMOOTH_IIR_2: + smooth_level_s = "IIR_2"; + break; + case TOUCH_PAD_SMOOTH_IIR_4: + smooth_level_s = "IIR_4"; + break; + case TOUCH_PAD_SMOOTH_IIR_8: + smooth_level_s = "IIR_8"; + break; + default: + smooth_level_s = "UNKNOWN"; + break; + } + ESP_LOGCONFIG(TAG, " Smooth level: %s", smooth_level_s); + } + + if (this->denoise_configured_()) { + const char *grade_s; + switch (this->grade_) { + case TOUCH_PAD_DENOISE_BIT12: + grade_s = "BIT12"; + break; + case TOUCH_PAD_DENOISE_BIT10: + grade_s = "BIT10"; + break; + case TOUCH_PAD_DENOISE_BIT8: + grade_s = "BIT8"; + break; + case TOUCH_PAD_DENOISE_BIT4: + grade_s = "BIT4"; + break; + default: + grade_s = "UNKNOWN"; + break; + } + ESP_LOGCONFIG(TAG, " Denoise grade: %s", grade_s); + + const char *cap_level_s; + switch (this->cap_level_) { + case TOUCH_PAD_DENOISE_CAP_L0: + cap_level_s = "L0"; + break; + case TOUCH_PAD_DENOISE_CAP_L1: + cap_level_s = "L1"; + break; + case TOUCH_PAD_DENOISE_CAP_L2: + cap_level_s = "L2"; + break; + case TOUCH_PAD_DENOISE_CAP_L3: + cap_level_s = "L3"; + break; + case TOUCH_PAD_DENOISE_CAP_L4: + cap_level_s = "L4"; + break; + case TOUCH_PAD_DENOISE_CAP_L5: + cap_level_s = "L5"; + break; + case TOUCH_PAD_DENOISE_CAP_L6: + cap_level_s = "L6"; + break; + case TOUCH_PAD_DENOISE_CAP_L7: + cap_level_s = "L7"; + break; + default: + cap_level_s = "UNKNOWN"; + break; + } + ESP_LOGCONFIG(TAG, " Denoise capacitance level: %s", cap_level_s); + } + + if (this->setup_mode_) { + ESP_LOGCONFIG(TAG, " Setup Mode ENABLED"); + } + + this->dump_config_sensors_(); +} + +void ESP32TouchComponent::loop() { + const uint32_t now = App.get_loop_component_start_time(); + + // V2 TOUCH HANDLING: + // Due to unreliable INACTIVE interrupts on ESP32-S2/S3, we use a hybrid approach: + // 1. Process ACTIVE interrupts when pads are touched + // 2. Use timeout-based release detection (like v1) + // 3. But smarter than v1: verify actual state before releasing on timeout + // This prevents false releases if we missed interrupts + + // In setup mode, periodically log all pad values + this->process_setup_mode_logging_(now); + + // Process any queued touch events from interrupts + TouchPadEventV2 event; + while (xQueueReceive(this->touch_queue_, &event, 0) == pdTRUE) { + ESP_LOGD(TAG, "Event received, mask = 0x%" PRIx32 ", pad = %d", event.intr_mask, event.pad); + // Handle timeout events + if (event.intr_mask & TOUCH_PAD_INTR_MASK_TIMEOUT) { + // Resume measurement after timeout + touch_pad_timeout_resume(); + // For timeout events, always check the current state + } else if (!(event.intr_mask & TOUCH_PAD_INTR_MASK_ACTIVE)) { + // Skip if not an active/timeout event + continue; + } + + // Find the child for the pad that triggered the interrupt + for (auto *child : this->children_) { + if (child->touch_pad_ == event.pad) { + if (event.intr_mask & TOUCH_PAD_INTR_MASK_TIMEOUT) { + // For timeout events, we need to read the value to determine state + this->check_and_update_touch_state_(child); + } else if (event.intr_mask & TOUCH_PAD_INTR_MASK_ACTIVE) { + // We only get ACTIVE interrupts now, releases are detected by timeout + this->update_touch_state_(child, true); // Always touched for ACTIVE interrupts + } + break; + } + } + } + + // Check for released pads periodically (like v1) + if (!this->should_check_for_releases_(now)) { + return; + } + + size_t pads_off = 0; + for (auto *child : this->children_) { + if (child->benchmark_ == 0) + touch_pad_read_benchmark(child->touch_pad_, &child->benchmark_); + // Handle initial state publication after startup + this->publish_initial_state_if_needed_(child, now); + + if (child->last_state_) { + // Pad is currently in touched state - check for release timeout + // Using subtraction handles 32-bit rollover correctly + uint32_t time_diff = now - child->last_touch_time_; + + // Check if we haven't seen this pad recently + if (time_diff > this->release_timeout_ms_) { + // Haven't seen this pad recently - verify actual state + // Unlike v1, v2 hardware allows us to read the current state anytime + // This makes v2 smarter: we can verify if it's actually released before + // declaring a timeout, preventing false releases if interrupts were missed + bool still_touched = this->check_and_update_touch_state_(child); + + if (still_touched) { + // Still touched! Timer was reset in update_touch_state_ + ESP_LOGVV(TAG, "Touch Pad '%s' still touched after %" PRIu32 "ms timeout, resetting timer", + child->get_name().c_str(), this->release_timeout_ms_); + } else { + // Actually released - already handled by check_and_update_touch_state_ + pads_off++; + } + } + } else { + // Pad is already off + pads_off++; + } + } + + // Disable the loop when all pads are off and not in setup mode (like v1) + // We need to keep checking for timeouts, so only disable when all pads are confirmed off + this->check_and_disable_loop_if_all_released_(pads_off); +} + +void ESP32TouchComponent::on_shutdown() { + // Disable interrupts + touch_pad_intr_disable(static_cast(TOUCH_PAD_INTR_MASK_ACTIVE | TOUCH_PAD_INTR_MASK_TIMEOUT)); + touch_pad_isr_deregister(touch_isr_handler, this); + this->cleanup_touch_queue_(); + + // Configure wakeup pads if any are set + this->configure_wakeup_pads_(); +} + +void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) { + ESP32TouchComponent *component = static_cast(arg); + BaseType_t x_higher_priority_task_woken = pdFALSE; + + // Read interrupt status + TouchPadEventV2 event; + event.intr_mask = touch_pad_read_intr_status_mask(); + event.pad = touch_pad_get_current_meas_channel(); + + // Send event to queue for processing in main loop + xQueueSendFromISR(component->touch_queue_, &event, &x_higher_priority_task_woken); + component->enable_loop_soon_any_context(); + + if (x_higher_priority_task_woken) { + portYIELD_FROM_ISR(); + } +} + +uint32_t ESP32TouchComponent::read_touch_value(touch_pad_t pad) const { + // Unlike ESP32 v1, touch reads on ESP32-S2/S3 v2 are non-blocking operations. + // The hardware continuously samples in the background and we can read the + // latest value at any time without waiting. + uint32_t value = 0; + if (this->filter_configured_()) { + // Read filtered/smoothed value when filter is enabled + touch_pad_filter_read_smooth(pad, &value); + } else { + // Read raw value when filter is not configured + touch_pad_read_raw_data(pad, &value); + } + return value; +} + +} // namespace esp32_touch +} // namespace esphome + +#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index c949e53aa6..81daad8c56 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -183,6 +183,7 @@ async def to_code(config): cg.add_platformio_option("board", config[CONF_BOARD]) cg.add_build_flag("-DUSE_ESP8266") + cg.set_cpp_standard("gnu++20") cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_define("ESPHOME_VARIANT", "ESP8266") diff --git a/esphome/components/esp8266/gpio.cpp b/esphome/components/esp8266/gpio.cpp index 4a997a790c..ee3683c67d 100644 --- a/esphome/components/esp8266/gpio.cpp +++ b/esphome/components/esp8266/gpio.cpp @@ -129,9 +129,9 @@ void IRAM_ATTR ISRInternalGPIOPin::digital_write(bool value) { } } else { if (value != arg->inverted) { - *arg->out_set_reg |= 1; + *arg->out_set_reg = *arg->out_set_reg | 1; } else { - *arg->out_set_reg &= ~1; + *arg->out_set_reg = *arg->out_set_reg & ~1; } } } @@ -147,7 +147,7 @@ void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) { if (flags & gpio::FLAG_OUTPUT) { *arg->mode_set_reg = arg->mask; if (flags & gpio::FLAG_OPEN_DRAIN) { - *arg->control_reg |= 1 << GPCD; + *arg->control_reg = *arg->control_reg | (1 << GPCD); } else { *arg->control_reg &= ~(1 << GPCD); } @@ -155,21 +155,21 @@ void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) { *arg->mode_clr_reg = arg->mask; } if (flags & gpio::FLAG_PULLUP) { - *arg->func_reg |= 1 << GPFPU; - *arg->control_reg |= 1 << GPCD; + *arg->func_reg = *arg->func_reg | (1 << GPFPU); + *arg->control_reg = *arg->control_reg | (1 << GPCD); } else { - *arg->func_reg &= ~(1 << GPFPU); + *arg->func_reg = *arg->func_reg & ~(1 << GPFPU); } } else { if (flags & gpio::FLAG_OUTPUT) { - *arg->mode_set_reg |= 1; + *arg->mode_set_reg = *arg->mode_set_reg | 1; } else { - *arg->mode_set_reg &= ~1; + *arg->mode_set_reg = *arg->mode_set_reg & ~1; } if (flags & gpio::FLAG_PULLDOWN) { - *arg->func_reg |= 1 << GP16FPD; + *arg->func_reg = *arg->func_reg | (1 << GP16FPD); } else { - *arg->func_reg &= ~(1 << GP16FPD); + *arg->func_reg = *arg->func_reg & ~(1 << GP16FPD); } } } diff --git a/esphome/components/esp8266/helpers.cpp b/esphome/components/esp8266/helpers.cpp new file mode 100644 index 0000000000..993de710c6 --- /dev/null +++ b/esphome/components/esp8266/helpers.cpp @@ -0,0 +1,31 @@ +#include "esphome/core/helpers.h" + +#ifdef USE_ESP8266 + +#include +#include +// for xt_rsil()/xt_wsr_ps() +#include + +namespace esphome { + +uint32_t random_uint32() { return os_random(); } +bool random_bytes(uint8_t *data, size_t len) { return os_get_random(data, len) == 0; } + +// ESP8266 doesn't have mutexes, but that shouldn't be an issue as it's single-core and non-preemptive OS. +Mutex::Mutex() {} +Mutex::~Mutex() {} +void Mutex::lock() {} +bool Mutex::try_lock() { return true; } +void Mutex::unlock() {} + +IRAM_ATTR InterruptLock::InterruptLock() { state_ = xt_rsil(15); } +IRAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(state_); } + +void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter) + wifi_get_macaddr(STATION_IF, mac); +} + +} // namespace esphome + +#endif // USE_ESP8266 diff --git a/esphome/components/esphome/ota/__init__.py b/esphome/components/esphome/ota/__init__.py index 86006e3e18..901657ec82 100644 --- a/esphome/components/esphome/ota/__init__.py +++ b/esphome/components/esphome/ota/__init__.py @@ -100,6 +100,7 @@ CONFIG_SCHEMA = ( esp32=3232, rp2040=2040, bk72xx=8892, + ln882x=8820, rtl87xx=8892, ): cv.port, cv.Optional(CONF_PASSWORD): cv.string, diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 227cb676ff..4cc82b9094 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -26,19 +26,19 @@ void ESPHomeOTAComponent::setup() { ota::register_ota_platform(this); #endif - server_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections - if (server_ == nullptr) { + this->server_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections + if (this->server_ == nullptr) { ESP_LOGW(TAG, "Could not create socket"); this->mark_failed(); return; } int enable = 1; - int err = server_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); + int err = this->server_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); if (err != 0) { ESP_LOGW(TAG, "Socket unable to set reuseaddr: errno %d", err); // we can still continue } - err = server_->setblocking(false); + err = this->server_->setblocking(false); if (err != 0) { ESP_LOGW(TAG, "Socket unable to set nonblocking mode: errno %d", err); this->mark_failed(); @@ -54,14 +54,14 @@ void ESPHomeOTAComponent::setup() { return; } - err = server_->bind((struct sockaddr *) &server, sizeof(server)); + err = this->server_->bind((struct sockaddr *) &server, sizeof(server)); if (err != 0) { ESP_LOGW(TAG, "Socket unable to bind: errno %d", errno); this->mark_failed(); return; } - err = server_->listen(4); + err = this->server_->listen(4); if (err != 0) { ESP_LOGW(TAG, "Socket unable to listen: errno %d", errno); this->mark_failed(); @@ -82,7 +82,14 @@ void ESPHomeOTAComponent::dump_config() { #endif } -void ESPHomeOTAComponent::loop() { this->handle_(); } +void ESPHomeOTAComponent::loop() { + // Skip handle_() call if no client connected and no incoming connections + // This optimization reduces idle loop overhead when OTA is not active + // Note: No need to check server_ for null as the component is marked failed in setup() if server_ creation fails + if (this->client_ != nullptr || this->server_->ready()) { + this->handle_(); + } +} static const uint8_t FEATURE_SUPPORTS_COMPRESSION = 0x01; @@ -101,23 +108,21 @@ void ESPHomeOTAComponent::handle_() { size_t size_acknowledged = 0; #endif - if (client_ == nullptr) { - // Check if the server socket is ready before accepting - if (this->server_->ready()) { - struct sockaddr_storage source_addr; - socklen_t addr_len = sizeof(source_addr); - client_ = server_->accept((struct sockaddr *) &source_addr, &addr_len); - } + if (this->client_ == nullptr) { + // We already checked server_->ready() in loop(), so we can accept directly + struct sockaddr_storage source_addr; + socklen_t addr_len = sizeof(source_addr); + this->client_ = this->server_->accept((struct sockaddr *) &source_addr, &addr_len); + if (this->client_ == nullptr) + return; } - if (client_ == nullptr) - return; int enable = 1; - int err = client_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int)); + int err = this->client_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int)); if (err != 0) { ESP_LOGW(TAG, "Socket could not enable TCP nodelay, errno %d", errno); - client_->close(); - client_ = nullptr; + this->client_->close(); + this->client_ = nullptr; return; } diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index cd77ea6053..ac07d02e37 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -23,8 +23,10 @@ from esphome.const import ( CONF_INTERRUPT_PIN, CONF_MANUAL_IP, CONF_MISO_PIN, + CONF_MODE, CONF_MOSI_PIN, CONF_PAGE_ID, + CONF_PIN, CONF_POLLING_INTERVAL, CONF_RESET_PIN, CONF_SPI, @@ -49,6 +51,7 @@ PHYRegister = ethernet_ns.struct("PHYRegister") CONF_PHY_ADDR = "phy_addr" CONF_MDC_PIN = "mdc_pin" CONF_MDIO_PIN = "mdio_pin" +CONF_CLK = "clk" CONF_CLK_MODE = "clk_mode" CONF_POWER_PIN = "power_pin" CONF_PHY_REGISTERS = "phy_registers" @@ -66,32 +69,25 @@ ETHERNET_TYPES = { "KSZ8081RNA": EthernetType.ETHERNET_TYPE_KSZ8081RNA, "W5500": EthernetType.ETHERNET_TYPE_W5500, "OPENETH": EthernetType.ETHERNET_TYPE_OPENETH, + "DM9051": EthernetType.ETHERNET_TYPE_DM9051, } -SPI_ETHERNET_TYPES = ["W5500"] +SPI_ETHERNET_TYPES = ["W5500", "DM9051"] SPI_ETHERNET_DEFAULT_POLLING_INTERVAL = TimePeriodMilliseconds(milliseconds=10) emac_rmii_clock_mode_t = cg.global_ns.enum("emac_rmii_clock_mode_t") -emac_rmii_clock_gpio_t = cg.global_ns.enum("emac_rmii_clock_gpio_t") + CLK_MODES = { - "GPIO0_IN": ( - emac_rmii_clock_mode_t.EMAC_CLK_EXT_IN, - emac_rmii_clock_gpio_t.EMAC_CLK_IN_GPIO, - ), - "GPIO0_OUT": ( - emac_rmii_clock_mode_t.EMAC_CLK_OUT, - emac_rmii_clock_gpio_t.EMAC_APPL_CLK_OUT_GPIO, - ), - "GPIO16_OUT": ( - emac_rmii_clock_mode_t.EMAC_CLK_OUT, - emac_rmii_clock_gpio_t.EMAC_CLK_OUT_GPIO, - ), - "GPIO17_OUT": ( - emac_rmii_clock_mode_t.EMAC_CLK_OUT, - emac_rmii_clock_gpio_t.EMAC_CLK_OUT_180_GPIO, - ), + "CLK_EXT_IN": emac_rmii_clock_mode_t.EMAC_CLK_EXT_IN, + "CLK_OUT": emac_rmii_clock_mode_t.EMAC_CLK_OUT, } +CLK_MODES_DEPRECATED = { + "GPIO0_IN": ("CLK_EXT_IN", 0), + "GPIO0_OUT": ("CLK_OUT", 0), + "GPIO16_OUT": ("CLK_OUT", 16), + "GPIO17_OUT": ("CLK_OUT", 17), +} MANUAL_IP_SCHEMA = cv.Schema( { @@ -153,6 +149,18 @@ def _validate(config): f"({CORE.target_framework} {CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]}), " f"'{CONF_INTERRUPT_PIN}' is a required option for [ethernet]." ) + elif config[CONF_TYPE] != "OPENETH": + if CONF_CLK_MODE in config: + LOGGER.warning( + "[ethernet] The 'clk_mode' option is deprecated and will be removed in ESPHome 2026.1. " + "Please update your configuration to use 'clk' instead." + ) + mode = CLK_MODES_DEPRECATED[config[CONF_CLK_MODE]] + config[CONF_CLK] = CLK_SCHEMA({CONF_MODE: mode[0], CONF_PIN: mode[1]}) + del config[CONF_CLK_MODE] + elif CONF_CLK not in config: + raise cv.Invalid("'clk' is a required option for [ethernet].") + return config @@ -176,14 +184,21 @@ PHY_REGISTER_SCHEMA = cv.Schema( cv.Optional(CONF_PAGE_ID): cv.hex_int, } ) +CLK_SCHEMA = cv.Schema( + { + cv.Required(CONF_MODE): cv.enum(CLK_MODES, upper=True, space="_"), + cv.Required(CONF_PIN): pins.internal_gpio_pin_number, + } +) RMII_SCHEMA = BASE_SCHEMA.extend( cv.Schema( { cv.Required(CONF_MDC_PIN): pins.internal_gpio_output_pin_number, cv.Required(CONF_MDIO_PIN): pins.internal_gpio_output_pin_number, - cv.Optional(CONF_CLK_MODE, default="GPIO0_IN"): cv.enum( - CLK_MODES, upper=True, space="_" + cv.Optional(CONF_CLK_MODE): cv.enum( + CLK_MODES_DEPRECATED, upper=True, space="_" ), + cv.Optional(CONF_CLK): CLK_SCHEMA, cv.Optional(CONF_PHY_ADDR, default=0): cv.int_range(min=0, max=31), cv.Optional(CONF_POWER_PIN): pins.internal_gpio_output_pin_number, cv.Optional(CONF_PHY_REGISTERS): cv.ensure_list(PHY_REGISTER_SCHEMA), @@ -224,6 +239,7 @@ CONFIG_SCHEMA = cv.All( "KSZ8081RNA": RMII_SCHEMA, "W5500": SPI_SCHEMA, "OPENETH": BASE_SCHEMA, + "DM9051": SPI_SCHEMA, }, upper=True, ), @@ -278,7 +294,7 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - if config[CONF_TYPE] == "W5500": + if config[CONF_TYPE] in SPI_ETHERNET_TYPES: cg.add(var.set_clk_pin(config[CONF_CLK_PIN])) cg.add(var.set_miso_pin(config[CONF_MISO_PIN])) cg.add(var.set_mosi_pin(config[CONF_MOSI_PIN])) @@ -296,7 +312,9 @@ async def to_code(config): cg.add_define("USE_ETHERNET_SPI") if CORE.using_esp_idf: add_idf_sdkconfig_option("CONFIG_ETH_USE_SPI_ETHERNET", True) - add_idf_sdkconfig_option("CONFIG_ETH_SPI_ETHERNET_W5500", True) + add_idf_sdkconfig_option( + f"CONFIG_ETH_SPI_ETHERNET_{config[CONF_TYPE]}", True + ) elif config[CONF_TYPE] == "OPENETH": cg.add_define("USE_ETHERNET_OPENETH") add_idf_sdkconfig_option("CONFIG_ETH_USE_OPENETH", True) @@ -304,7 +322,8 @@ async def to_code(config): cg.add(var.set_phy_addr(config[CONF_PHY_ADDR])) cg.add(var.set_mdc_pin(config[CONF_MDC_PIN])) cg.add(var.set_mdio_pin(config[CONF_MDIO_PIN])) - cg.add(var.set_clk_mode(*CLK_MODES[config[CONF_CLK_MODE]])) + cg.add(var.set_clk_mode(config[CONF_CLK][CONF_MODE])) + cg.add(var.set_clk_pin(config[CONF_CLK][CONF_PIN])) if CONF_POWER_PIN in config: cg.add(var.set_power_pin(config[CONF_POWER_PIN])) for register_value in config.get(CONF_PHY_REGISTERS, []): diff --git a/esphome/components/ethernet/esp_eth_phy_jl1101.c b/esphome/components/ethernet/esp_eth_phy_jl1101.c index de2a6f4f35..4f31e0a9fd 100644 --- a/esphome/components/ethernet/esp_eth_phy_jl1101.c +++ b/esphome/components/ethernet/esp_eth_phy_jl1101.c @@ -19,11 +19,7 @@ #include #include "esp_log.h" #include "esp_eth.h" -#if ESP_IDF_VERSION_MAJOR >= 5 #include "esp_eth_phy_802_3.h" -#else -#include "eth_phy_regs_struct.h" -#endif #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "driver/gpio.h" @@ -174,11 +170,7 @@ static esp_err_t jl1101_reset_hw(esp_eth_phy_t *phy) { return ESP_OK; } -#if ESP_IDF_VERSION_MAJOR >= 5 static esp_err_t jl1101_negotiate(esp_eth_phy_t *phy, eth_phy_autoneg_cmd_t cmd, bool *nego_state) { -#else -static esp_err_t jl1101_negotiate(esp_eth_phy_t *phy) { -#endif phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent); esp_eth_mediator_t *eth = jl1101->eth; /* in case any link status has changed, let's assume we're in link down status */ @@ -293,11 +285,7 @@ static esp_err_t jl1101_init(esp_eth_phy_t *phy) { esp_eth_mediator_t *eth = jl1101->eth; // Detect PHY address if (jl1101->addr == ESP_ETH_PHY_ADDR_AUTO) { -#if ESP_IDF_VERSION_MAJOR >= 5 PHY_CHECK(esp_eth_phy_802_3_detect_phy_addr(eth, &jl1101->addr) == ESP_OK, "Detect PHY address failed", err); -#else - PHY_CHECK(esp_eth_detect_phy_addr(eth, &jl1101->addr) == ESP_OK, "Detect PHY address failed", err); -#endif } /* Power on Ethernet PHY */ PHY_CHECK(jl1101_pwrctl(phy, true) == ESP_OK, "power control failed", err); @@ -336,11 +324,7 @@ esp_eth_phy_t *esp_eth_phy_new_jl1101(const eth_phy_config_t *config) { jl1101->parent.init = jl1101_init; jl1101->parent.deinit = jl1101_deinit; jl1101->parent.set_mediator = jl1101_set_mediator; -#if ESP_IDF_VERSION_MAJOR >= 5 jl1101->parent.autonego_ctrl = jl1101_negotiate; -#else - jl1101->parent.negotiate = jl1101_negotiate; -#endif jl1101->parent.get_link = jl1101_get_link; jl1101->parent.pwrctl = jl1101_pwrctl; jl1101->parent.get_addr = jl1101_get_addr; diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index fe96973924..f8c2f3a72e 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -17,6 +17,22 @@ namespace esphome { namespace ethernet { +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) +// work around IDF compile issue on P4 https://github.com/espressif/esp-idf/pull/15637 +#ifdef USE_ESP32_VARIANT_ESP32P4 +#undef ETH_ESP32_EMAC_DEFAULT_CONFIG +#define ETH_ESP32_EMAC_DEFAULT_CONFIG() \ + { \ + .smi_gpio = {.mdc_num = 31, .mdio_num = 52}, .interface = EMAC_DATA_INTERFACE_RMII, \ + .clock_config = {.rmii = {.clock_mode = EMAC_CLK_EXT_IN, .clock_gpio = (emac_rmii_clock_gpio_t) 50}}, \ + .dma_burst_len = ETH_DMA_BURST_LEN_32, .intr_priority = 0, \ + .emac_dataif_gpio = \ + {.rmii = {.tx_en_num = 49, .txd0_num = 34, .txd1_num = 35, .crs_dv_num = 28, .rxd0_num = 29, .rxd1_num = 30}}, \ + .clock_config_out_in = {.rmii = {.clock_mode = EMAC_CLK_EXT_IN, .clock_gpio = (emac_rmii_clock_gpio_t) -1}}, \ + } +#endif +#endif + static const char *const TAG = "ethernet"; EthernetComponent *global_eth_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) @@ -90,8 +106,8 @@ void EthernetComponent::setup() { #ifdef USE_ETHERNET_SPI // Configure SPI interface and Ethernet driver for specific SPI module spi_device_interface_config_t devcfg = { - .command_bits = 16, // Actually it's the address phase in W5500 SPI frame - .address_bits = 8, // Actually it's the control phase in W5500 SPI frame + .command_bits = 0, + .address_bits = 0, .dummy_bits = 0, .mode = 0, .duty_cycle_pos = 0, @@ -106,45 +122,49 @@ void EthernetComponent::setup() { .post_cb = nullptr, }; -#if USE_ESP_IDF && (ESP_IDF_VERSION_MAJOR >= 5) +#if CONFIG_ETH_SPI_ETHERNET_W5500 eth_w5500_config_t w5500_config = ETH_W5500_DEFAULT_CONFIG(host, &devcfg); -#else - spi_device_handle_t spi_handle = nullptr; - err = spi_bus_add_device(host, &devcfg, &spi_handle); - ESPHL_ERROR_CHECK(err, "SPI bus add device error"); - - eth_w5500_config_t w5500_config = ETH_W5500_DEFAULT_CONFIG(spi_handle); #endif +#if CONFIG_ETH_SPI_ETHERNET_DM9051 + eth_dm9051_config_t dm9051_config = ETH_DM9051_DEFAULT_CONFIG(host, &devcfg); +#endif + +#if CONFIG_ETH_SPI_ETHERNET_W5500 w5500_config.int_gpio_num = this->interrupt_pin_; #ifdef USE_ETHERNET_SPI_POLLING_SUPPORT w5500_config.poll_period_ms = this->polling_interval_; #endif +#endif + +#if CONFIG_ETH_SPI_ETHERNET_DM9051 + dm9051_config.int_gpio_num = this->interrupt_pin_; +#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT + dm9051_config.poll_period_ms = this->polling_interval_; +#endif +#endif + phy_config.phy_addr = this->phy_addr_spi_; phy_config.reset_gpio_num = this->reset_pin_; - esp_eth_mac_t *mac = esp_eth_mac_new_w5500(&w5500_config, &mac_config); + esp_eth_mac_t *mac = nullptr; #elif defined(USE_ETHERNET_OPENETH) esp_eth_mac_t *mac = esp_eth_mac_new_openeth(&mac_config); #else phy_config.phy_addr = this->phy_addr_; phy_config.reset_gpio_num = this->power_pin_; -#if ESP_IDF_VERSION_MAJOR >= 5 eth_esp32_emac_config_t esp32_emac_config = ETH_ESP32_EMAC_DEFAULT_CONFIG(); +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) + esp32_emac_config.smi_gpio.mdc_num = this->mdc_pin_; + esp32_emac_config.smi_gpio.mdio_num = this->mdio_pin_; +#else esp32_emac_config.smi_mdc_gpio_num = this->mdc_pin_; esp32_emac_config.smi_mdio_gpio_num = this->mdio_pin_; +#endif esp32_emac_config.clock_config.rmii.clock_mode = this->clk_mode_; - esp32_emac_config.clock_config.rmii.clock_gpio = this->clk_gpio_; + esp32_emac_config.clock_config.rmii.clock_gpio = (emac_rmii_clock_gpio_t) this->clk_pin_; esp_eth_mac_t *mac = esp_eth_mac_new_esp32(&esp32_emac_config, &mac_config); -#else - mac_config.smi_mdc_gpio_num = this->mdc_pin_; - mac_config.smi_mdio_gpio_num = this->mdio_pin_; - mac_config.clock_config.rmii.clock_mode = this->clk_mode_; - mac_config.clock_config.rmii.clock_gpio = this->clk_gpio_; - - esp_eth_mac_t *mac = esp_eth_mac_new_esp32(&mac_config); -#endif #endif switch (this->type_) { @@ -178,19 +198,25 @@ void EthernetComponent::setup() { } case ETHERNET_TYPE_KSZ8081: case ETHERNET_TYPE_KSZ8081RNA: { -#if ESP_IDF_VERSION_MAJOR >= 5 this->phy_ = esp_eth_phy_new_ksz80xx(&phy_config); -#else - this->phy_ = esp_eth_phy_new_ksz8081(&phy_config); -#endif break; } #endif #ifdef USE_ETHERNET_SPI +#if CONFIG_ETH_SPI_ETHERNET_W5500 case ETHERNET_TYPE_W5500: { + mac = esp_eth_mac_new_w5500(&w5500_config, &mac_config); this->phy_ = esp_eth_phy_new_w5500(&phy_config); break; } +#endif +#if CONFIG_ETH_SPI_ETHERNET_DM9051 + case ETHERNET_TYPE_DM9051: { + mac = esp_eth_mac_new_dm9051(&dm9051_config, &mac_config); + this->phy_ = esp_eth_phy_new_dm9051(&phy_config); + break; + } +#endif #endif default: { this->mark_failed(); @@ -274,6 +300,9 @@ void EthernetComponent::loop() { ESP_LOGW(TAG, "Connection lost; reconnecting"); this->state_ = EthernetComponentState::CONNECTING; this->start_connect_(); + } else { + // When connected and stable, disable the loop to save CPU cycles + this->disable_loop(); } break; } @@ -318,6 +347,10 @@ void EthernetComponent::dump_config() { eth_type = "OPENETH"; break; + case ETHERNET_TYPE_DM9051: + eth_type = "DM9051"; + break; + default: eth_type = "Unknown"; break; @@ -349,10 +382,11 @@ void EthernetComponent::dump_config() { ESP_LOGCONFIG(TAG, " Power Pin: %u", this->power_pin_); } ESP_LOGCONFIG(TAG, + " CLK Pin: %u\n" " MDC Pin: %u\n" " MDIO Pin: %u\n" " PHY addr: %u", - this->mdc_pin_, this->mdio_pin_, this->phy_addr_); + this->clk_pin_, this->mdc_pin_, this->mdio_pin_, this->phy_addr_); #endif ESP_LOGCONFIG(TAG, " Type: %s", eth_type); } @@ -397,11 +431,13 @@ void EthernetComponent::eth_event_handler(void *arg, esp_event_base_t event_base case ETHERNET_EVENT_START: event_name = "ETH started"; global_eth_component->started_ = true; + global_eth_component->enable_loop_soon_any_context(); break; case ETHERNET_EVENT_STOP: event_name = "ETH stopped"; global_eth_component->started_ = false; global_eth_component->connected_ = false; + global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes break; case ETHERNET_EVENT_CONNECTED: event_name = "ETH connected"; @@ -409,6 +445,7 @@ void EthernetComponent::eth_event_handler(void *arg, esp_event_base_t event_base case ETHERNET_EVENT_DISCONNECTED: event_name = "ETH disconnected"; global_eth_component->connected_ = false; + global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes break; default: return; @@ -425,8 +462,10 @@ void EthernetComponent::got_ip_event_handler(void *arg, esp_event_base_t event_b global_eth_component->got_ipv4_address_ = true; #if USE_NETWORK_IPV6 && (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0) global_eth_component->connected_ = global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT; + global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes #else global_eth_component->connected_ = true; + global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes #endif /* USE_NETWORK_IPV6 */ } @@ -439,8 +478,10 @@ void EthernetComponent::got_ip6_event_handler(void *arg, esp_event_base_t event_ #if (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0) global_eth_component->connected_ = global_eth_component->got_ipv4_address_ && (global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT); + global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes #else global_eth_component->connected_ = global_eth_component->got_ipv4_address_; + global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes #endif } #endif /* USE_NETWORK_IPV6 */ @@ -566,10 +607,8 @@ void EthernetComponent::set_phy_addr(uint8_t phy_addr) { this->phy_addr_ = phy_a void EthernetComponent::set_power_pin(int power_pin) { this->power_pin_ = power_pin; } void EthernetComponent::set_mdc_pin(uint8_t mdc_pin) { this->mdc_pin_ = mdc_pin; } void EthernetComponent::set_mdio_pin(uint8_t mdio_pin) { this->mdio_pin_ = mdio_pin; } -void EthernetComponent::set_clk_mode(emac_rmii_clock_mode_t clk_mode, emac_rmii_clock_gpio_t clk_gpio) { - this->clk_mode_ = clk_mode; - this->clk_gpio_ = clk_gpio; -} +void EthernetComponent::set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; } +void EthernetComponent::set_clk_mode(emac_rmii_clock_mode_t clk_mode) { this->clk_mode_ = clk_mode; } void EthernetComponent::add_phy_register(PHYRegister register_value) { this->phy_registers_.push_back(register_value); } #endif void EthernetComponent::set_type(EthernetType type) { this->type_ = type; } @@ -620,6 +659,7 @@ bool EthernetComponent::powerdown() { } this->connected_ = false; this->started_ = false; + // No need to enable_loop() here as this is only called during shutdown/reboot if (this->phy_->pwrctl(this->phy_, false) != ESP_OK) { ESP_LOGE(TAG, "Error powering down ethernet PHY"); return false; diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index 7a205d89f0..1b347946f5 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -15,7 +15,7 @@ namespace esphome { namespace ethernet { -enum EthernetType { +enum EthernetType : uint8_t { ETHERNET_TYPE_UNKNOWN = 0, ETHERNET_TYPE_LAN8720, ETHERNET_TYPE_RTL8201, @@ -26,6 +26,7 @@ enum EthernetType { ETHERNET_TYPE_KSZ8081RNA, ETHERNET_TYPE_W5500, ETHERNET_TYPE_OPENETH, + ETHERNET_TYPE_DM9051, }; struct ManualIP { @@ -42,7 +43,7 @@ struct PHYRegister { uint32_t page; }; -enum class EthernetComponentState { +enum class EthernetComponentState : uint8_t { STOPPED, CONNECTING, CONNECTED, @@ -75,7 +76,8 @@ class EthernetComponent : public Component { void set_power_pin(int power_pin); void set_mdc_pin(uint8_t mdc_pin); void set_mdio_pin(uint8_t mdio_pin); - void set_clk_mode(emac_rmii_clock_mode_t clk_mode, emac_rmii_clock_gpio_t clk_gpio); + void set_clk_pin(uint8_t clk_pin); + void set_clk_mode(emac_rmii_clock_mode_t clk_mode); void add_phy_register(PHYRegister register_value); #endif void set_type(EthernetType type); @@ -119,25 +121,31 @@ class EthernetComponent : public Component { uint32_t polling_interval_{0}; #endif #else - uint8_t phy_addr_{0}; + // Group all 32-bit members first int power_pin_{-1}; + emac_rmii_clock_mode_t clk_mode_{EMAC_CLK_EXT_IN}; + std::vector phy_registers_{}; + + // Group all 8-bit members together + uint8_t clk_pin_{0}; + uint8_t phy_addr_{0}; uint8_t mdc_pin_{23}; uint8_t mdio_pin_{18}; - emac_rmii_clock_mode_t clk_mode_{EMAC_CLK_EXT_IN}; - emac_rmii_clock_gpio_t clk_gpio_{EMAC_CLK_IN_GPIO}; - std::vector phy_registers_{}; #endif - EthernetType type_{ETHERNET_TYPE_UNKNOWN}; optional manual_ip_{}; + uint32_t connect_begin_; + // Group all uint8_t types together (enums and bools) + EthernetType type_{ETHERNET_TYPE_UNKNOWN}; + EthernetComponentState state_{EthernetComponentState::STOPPED}; bool started_{false}; bool connected_{false}; bool got_ipv4_address_{false}; #if LWIP_IPV6 uint8_t ipv6_count_{0}; #endif /* LWIP_IPV6 */ - EthernetComponentState state_{EthernetComponentState::STOPPED}; - uint32_t connect_begin_; + + // Pointers at the end (naturally aligned) esp_netif_t *eth_netif_{nullptr}; esp_eth_handle_t eth_handle_; esp_eth_phy_t *phy_{nullptr}; diff --git a/esphome/components/event/__init__.py b/esphome/components/event/__init__.py index e7ab489a25..3aff96a48e 100644 --- a/esphome/components/event/__init__.py +++ b/esphome/components/event/__init__.py @@ -18,8 +18,8 @@ from esphome.const import ( DEVICE_CLASS_MOTION, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@nohat"] IS_PLATFORM_COMPONENT = True @@ -59,6 +59,9 @@ _EVENT_SCHEMA = ( ) +_EVENT_SCHEMA.add_extra(entity_duplicate_validator("event")) + + def event_schema( class_: MockObjClass = cv.UNDEFINED, *, @@ -88,7 +91,7 @@ EVENT_SCHEMA.add_extra(cv.deprecated_schema_constant("event")) async def setup_event_core_(var, config, *, event_types: list[str]): - await setup_entity(var, config) + await setup_entity(var, config, "event") for conf in config.get(CONF_ON_EVENT, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) diff --git a/esphome/components/ezo/ezo.h b/esphome/components/ezo/ezo.h index 28b46643e9..00dd98fc80 100644 --- a/esphome/components/ezo/ezo.h +++ b/esphome/components/ezo/ezo.h @@ -38,7 +38,6 @@ class EZOSensor : public sensor::Sensor, public PollingComponent, public i2c::I2 void loop() override; void dump_config() override; void update() override; - float get_setup_priority() const override { return setup_priority::DATA; }; // I2C void set_address(uint8_t address); diff --git a/esphome/components/ezo_pmp/ezo_pmp.h b/esphome/components/ezo_pmp/ezo_pmp.h index b41710cd78..671e124810 100644 --- a/esphome/components/ezo_pmp/ezo_pmp.h +++ b/esphome/components/ezo_pmp/ezo_pmp.h @@ -23,7 +23,6 @@ namespace ezo_pmp { class EzoPMP : public PollingComponent, public i2c::I2CDevice { public: void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; }; void loop() override; void update() override; diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py index c6ff938cd6..0b1d39575d 100644 --- a/esphome/components/fan/__init__.py +++ b/esphome/components/fan/__init__.py @@ -32,7 +32,7 @@ from esphome.const import ( CONF_WEB_SERVER, ) from esphome.core import CORE, coroutine_with_priority -from esphome.cpp_helpers import setup_entity +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity IS_PLATFORM_COMPONENT = True @@ -161,6 +161,9 @@ _FAN_SCHEMA = ( ) +_FAN_SCHEMA.add_extra(entity_duplicate_validator("fan")) + + def fan_schema( class_: cg.Pvariable, *, @@ -225,7 +228,7 @@ def validate_preset_modes(value): async def setup_fan_core_(var, config): - await setup_entity(var, config) + await setup_entity(var, config, "fan") cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE])) diff --git a/esphome/components/feedback/feedback_cover.h b/esphome/components/feedback/feedback_cover.h index 7e107aebcd..199d3b520a 100644 --- a/esphome/components/feedback/feedback_cover.h +++ b/esphome/components/feedback/feedback_cover.h @@ -16,7 +16,6 @@ class FeedbackCover : public cover::Cover, public Component { void setup() override; void loop() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; }; Trigger<> *get_open_trigger() const { return this->open_trigger_; } Trigger<> *get_close_trigger() const { return this->close_trigger_; } diff --git a/esphome/components/font/__init__.py b/esphome/components/font/__init__.py index be88fdb957..7d9a35647e 100644 --- a/esphome/components/font/__init__.py +++ b/esphome/components/font/__init__.py @@ -1,6 +1,7 @@ from collections.abc import MutableMapping import functools import hashlib +from itertools import accumulate import logging import os from pathlib import Path @@ -468,8 +469,9 @@ class EFont: class GlyphInfo: - def __init__(self, data_len, advance, offset_x, offset_y, width, height): - self.data_len = data_len + def __init__(self, glyph, data, advance, offset_x, offset_y, width, height): + self.glyph = glyph + self.bitmap_data = data self.advance = advance self.offset_x = offset_x self.offset_y = offset_y @@ -477,6 +479,62 @@ class GlyphInfo: self.height = height +def glyph_to_glyphinfo(glyph, font, size, bpp): + scale = 256 // (1 << bpp) + if not font.is_scalable: + sizes = [pt_to_px(x.size) for x in font.available_sizes] + if size in sizes: + font.select_size(sizes.index(size)) + else: + font.set_pixel_sizes(size, 0) + flags = FT_LOAD_RENDER + if bpp != 1: + flags |= FT_LOAD_NO_BITMAP + else: + flags |= FT_LOAD_TARGET_MONO + font.load_char(glyph, flags) + width = font.glyph.bitmap.width + height = font.glyph.bitmap.rows + buffer = font.glyph.bitmap.buffer + pitch = font.glyph.bitmap.pitch + glyph_data = [0] * ((height * width * bpp + 7) // 8) + src_mode = font.glyph.bitmap.pixel_mode + pos = 0 + for y in range(height): + for x in range(width): + if src_mode == ft_pixel_mode_mono: + pixel = ( + (1 << bpp) - 1 + if buffer[y * pitch + x // 8] & (1 << (7 - x % 8)) + else 0 + ) + else: + pixel = buffer[y * pitch + x] // scale + for bit_num in range(bpp): + if pixel & (1 << (bpp - bit_num - 1)): + glyph_data[pos // 8] |= 0x80 >> (pos % 8) + pos += 1 + ascender = pt_to_px(font.size.ascender) + if ascender == 0: + if not font.is_scalable: + ascender = size + else: + _LOGGER.error( + "Unable to determine ascender of font %s %s", + font.family_name, + font.style_name, + ) + return GlyphInfo( + glyph, + glyph_data, + pt_to_px(font.glyph.metrics.horiAdvance), + font.glyph.bitmap_left, + ascender - font.glyph.bitmap_top, + width, + height, + ) + + async def to_code(config): """ Collect all glyph codepoints, construct a map from a codepoint to a font file. @@ -506,98 +564,47 @@ async def to_code(config): codepoints = list(point_set) codepoints.sort(key=functools.cmp_to_key(glyph_comparator)) - glyph_args = {} - data = [] bpp = config[CONF_BPP] - scale = 256 // (1 << bpp) size = config[CONF_SIZE] # create the data array for all glyphs - for codepoint in codepoints: - font = point_font_map[codepoint] - if not font.is_scalable: - sizes = [pt_to_px(x.size) for x in font.available_sizes] - if size in sizes: - font.select_size(sizes.index(size)) - else: - font.set_pixel_sizes(size, 0) - flags = FT_LOAD_RENDER - if bpp != 1: - flags |= FT_LOAD_NO_BITMAP - else: - flags |= FT_LOAD_TARGET_MONO - font.load_char(codepoint, flags) - width = font.glyph.bitmap.width - height = font.glyph.bitmap.rows - buffer = font.glyph.bitmap.buffer - pitch = font.glyph.bitmap.pitch - glyph_data = [0] * ((height * width * bpp + 7) // 8) - src_mode = font.glyph.bitmap.pixel_mode - pos = 0 - for y in range(height): - for x in range(width): - if src_mode == ft_pixel_mode_mono: - pixel = ( - (1 << bpp) - 1 - if buffer[y * pitch + x // 8] & (1 << (7 - x % 8)) - else 0 - ) - else: - pixel = buffer[y * pitch + x] // scale - for bit_num in range(bpp): - if pixel & (1 << (bpp - bit_num - 1)): - glyph_data[pos // 8] |= 0x80 >> (pos % 8) - pos += 1 - ascender = pt_to_px(font.size.ascender) - if ascender == 0: - if not font.is_scalable: - ascender = size - else: - _LOGGER.error( - "Unable to determine ascender of font %s", config[CONF_FILE] - ) - glyph_args[codepoint] = GlyphInfo( - len(data), - pt_to_px(font.glyph.metrics.horiAdvance), - font.glyph.bitmap_left, - ascender - font.glyph.bitmap_top, - width, - height, - ) - data += glyph_data - - rhs = [HexInt(x) for x in data] + glyph_args = [ + glyph_to_glyphinfo(x, point_font_map[x], size, bpp) for x in codepoints + ] + rhs = [HexInt(x) for x in flatten([x.bitmap_data for x in glyph_args])] prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) # Create the glyph table that points to data in the above array. - glyph_initializer = [] - for codepoint in codepoints: - glyph_initializer.append( - cg.StructInitializer( - GlyphData, - ( - "a_char", - cg.RawExpression( - f"(const uint8_t *){cpp_string_escape(codepoint)}" - ), - ), - ( - "data", - cg.RawExpression( - f"{str(prog_arr)} + {str(glyph_args[codepoint].data_len)}" - ), - ), - ("advance", glyph_args[codepoint].advance), - ("offset_x", glyph_args[codepoint].offset_x), - ("offset_y", glyph_args[codepoint].offset_y), - ("width", glyph_args[codepoint].width), - ("height", glyph_args[codepoint].height), - ) + glyph_initializer = [ + cg.StructInitializer( + GlyphData, + ( + "a_char", + cg.RawExpression(f"(const uint8_t *){cpp_string_escape(x.glyph)}"), + ), + ( + "data", + cg.RawExpression(f"{str(prog_arr)} + {str(y - len(x.bitmap_data))}"), + ), + ("advance", x.advance), + ("offset_x", x.offset_x), + ("offset_y", x.offset_y), + ("width", x.width), + ("height", x.height), ) + for (x, y) in zip( + glyph_args, list(accumulate([len(x.bitmap_data) for x in glyph_args])) + ) + ] glyphs = cg.static_const_array(config[CONF_RAW_GLYPH_ID], glyph_initializer) font_height = pt_to_px(base_font.size.height) ascender = pt_to_px(base_font.size.ascender) + descender = abs(pt_to_px(base_font.size.descender)) + g = glyph_to_glyphinfo("x", base_font, size, bpp) + xheight = g.height if len(g.bitmap_data) > 1 else 0 + g = glyph_to_glyphinfo("X", base_font, size, bpp) + capheight = g.height if len(g.bitmap_data) > 1 else 0 if font_height == 0: if not base_font.is_scalable: font_height = size @@ -610,5 +617,8 @@ async def to_code(config): len(glyph_initializer), ascender, font_height, + descender, + xheight, + capheight, bpp, ) diff --git a/esphome/components/font/font.cpp b/esphome/components/font/font.cpp index 32464d87ee..8b2420ac07 100644 --- a/esphome/components/font/font.cpp +++ b/esphome/components/font/font.cpp @@ -45,8 +45,15 @@ void Glyph::scan_area(int *x1, int *y1, int *width, int *height) const { *height = this->glyph_data_->height; } -Font::Font(const GlyphData *data, int data_nr, int baseline, int height, uint8_t bpp) - : baseline_(baseline), height_(height), bpp_(bpp) { +Font::Font(const GlyphData *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight, + uint8_t bpp) + : baseline_(baseline), + height_(height), + descender_(descender), + linegap_(height - baseline - descender), + xheight_(xheight), + capheight_(capheight), + bpp_(bpp) { glyphs_.reserve(data_nr); for (int i = 0; i < data_nr; ++i) glyphs_.emplace_back(&data[i]); diff --git a/esphome/components/font/font.h b/esphome/components/font/font.h index 9ee23b3ec5..28832d647d 100644 --- a/esphome/components/font/font.h +++ b/esphome/components/font/font.h @@ -50,11 +50,17 @@ class Font public: /** Construct the font with the given glyphs. * - * @param glyphs A vector of glyphs, must be sorted lexicographically. + * @param data A vector of glyphs, must be sorted lexicographically. + * @param data_nr The number of glyphs in data. * @param baseline The y-offset from the top of the text to the baseline. - * @param bottom The y-offset from the top of the text to the bottom (i.e. height). + * @param height The y-offset from the top of the text to the bottom. + * @param descender The y-offset from the baseline to the lowest stroke in the font (e.g. from letters like g or p). + * @param xheight The height of lowercase letters, usually measured at the "x" glyph. + * @param capheight The height of capital letters, usually measured at the "X" glyph. + * @param bpp The bits per pixel used for this font. Used to read data out of the glyph bitmaps. */ - Font(const GlyphData *data, int data_nr, int baseline, int height, uint8_t bpp = 1); + Font(const GlyphData *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight, + uint8_t bpp = 1); int match_next_glyph(const uint8_t *str, int *match_length); @@ -65,14 +71,23 @@ class Font #endif inline int get_baseline() { return this->baseline_; } inline int get_height() { return this->height_; } + inline int get_ascender() { return this->baseline_; } + inline int get_descender() { return this->descender_; } + inline int get_linegap() { return this->linegap_; } + inline int get_xheight() { return this->xheight_; } + inline int get_capheight() { return this->capheight_; } inline int get_bpp() { return this->bpp_; } - const std::vector> &get_glyphs() const { return glyphs_; } + const std::vector> &get_glyphs() const { return glyphs_; } protected: - std::vector> glyphs_; + std::vector> glyphs_; int baseline_; int height_; + int descender_; + int linegap_; + int xheight_; + int capheight_; uint8_t bpp_; // bits per pixel }; diff --git a/esphome/components/fs3000/fs3000.h b/esphome/components/fs3000/fs3000.h index be3680e7e1..e33c72215f 100644 --- a/esphome/components/fs3000/fs3000.h +++ b/esphome/components/fs3000/fs3000.h @@ -18,7 +18,6 @@ class FS3000Component : public PollingComponent, public i2c::I2CDevice, public s void update() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_model(FS3000Model model) { this->model_ = model; } diff --git a/esphome/components/gcja5/gcja5.h b/esphome/components/gcja5/gcja5.h index ea1fb78bf0..30bc877169 100644 --- a/esphome/components/gcja5/gcja5.h +++ b/esphome/components/gcja5/gcja5.h @@ -12,7 +12,6 @@ class GCJA5Component : public Component, public uart::UARTDevice { public: void dump_config() override; void loop() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_pm_1_0_sensor(sensor::Sensor *pm_1_0) { pm_1_0_sensor_ = pm_1_0; } void set_pm_2_5_sensor(sensor::Sensor *pm_2_5) { pm_2_5_sensor_ = pm_2_5; } diff --git a/esphome/components/gl_r01_i2c/__init__.py b/esphome/components/gl_r01_i2c/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/gl_r01_i2c/gl_r01_i2c.cpp b/esphome/components/gl_r01_i2c/gl_r01_i2c.cpp new file mode 100644 index 0000000000..5a24c63525 --- /dev/null +++ b/esphome/components/gl_r01_i2c/gl_r01_i2c.cpp @@ -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 diff --git a/esphome/components/gl_r01_i2c/gl_r01_i2c.h b/esphome/components/gl_r01_i2c/gl_r01_i2c.h new file mode 100644 index 0000000000..9a7aa023fd --- /dev/null +++ b/esphome/components/gl_r01_i2c/gl_r01_i2c.h @@ -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 diff --git a/esphome/components/gl_r01_i2c/sensor.py b/esphome/components/gl_r01_i2c/sensor.py new file mode 100644 index 0000000000..9f6f75faf7 --- /dev/null +++ b/esphome/components/gl_r01_i2c/sensor.py @@ -0,0 +1,36 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +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) diff --git a/esphome/components/gp8403/gp8403.h b/esphome/components/gp8403/gp8403.h index 65182ef301..9f493d39e3 100644 --- a/esphome/components/gp8403/gp8403.h +++ b/esphome/components/gp8403/gp8403.h @@ -15,7 +15,6 @@ class GP8403 : public Component, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_voltage(gp8403::GP8403Voltage voltage) { this->voltage_ = voltage; } diff --git a/esphome/components/gpio/binary_sensor/__init__.py b/esphome/components/gpio/binary_sensor/__init__.py index 23f2781095..9f50fd779a 100644 --- a/esphome/components/gpio/binary_sensor/__init__.py +++ b/esphome/components/gpio/binary_sensor/__init__.py @@ -10,11 +10,24 @@ GPIOBinarySensor = gpio_ns.class_( "GPIOBinarySensor", binary_sensor.BinarySensor, cg.Component ) +CONF_USE_INTERRUPT = "use_interrupt" +CONF_INTERRUPT_TYPE = "interrupt_type" + +INTERRUPT_TYPES = { + "RISING": gpio_ns.INTERRUPT_RISING_EDGE, + "FALLING": gpio_ns.INTERRUPT_FALLING_EDGE, + "ANY": gpio_ns.INTERRUPT_ANY_EDGE, +} + CONFIG_SCHEMA = ( binary_sensor.binary_sensor_schema(GPIOBinarySensor) .extend( { cv.Required(CONF_PIN): pins.gpio_input_pin_schema, + cv.Optional(CONF_USE_INTERRUPT, default=True): cv.boolean, + cv.Optional(CONF_INTERRUPT_TYPE, default="ANY"): cv.enum( + INTERRUPT_TYPES, upper=True + ), } ) .extend(cv.COMPONENT_SCHEMA) @@ -27,3 +40,7 @@ async def to_code(config): pin = await cg.gpio_pin_expression(config[CONF_PIN]) cg.add(var.set_pin(pin)) + + cg.add(var.set_use_interrupt(config[CONF_USE_INTERRUPT])) + if config[CONF_USE_INTERRUPT]: + cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE])) diff --git a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp index cf4b088580..4b8369cd59 100644 --- a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp +++ b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp @@ -6,17 +6,91 @@ namespace gpio { static const char *const TAG = "gpio.binary_sensor"; +void IRAM_ATTR GPIOBinarySensorStore::gpio_intr(GPIOBinarySensorStore *arg) { + bool new_state = arg->isr_pin_.digital_read(); + if (new_state != arg->last_state_) { + arg->state_ = new_state; + arg->last_state_ = new_state; + arg->changed_ = true; + // Wake up the component from its disabled loop state + if (arg->component_ != nullptr) { + arg->component_->enable_loop_soon_any_context(); + } + } +} + +void GPIOBinarySensorStore::setup(InternalGPIOPin *pin, gpio::InterruptType type, Component *component) { + pin->setup(); + this->isr_pin_ = pin->to_isr(); + this->component_ = component; + + // Read initial state + this->last_state_ = pin->digital_read(); + this->state_ = this->last_state_; + + // Attach interrupt - from this point on, any changes will be caught by the interrupt + pin->attach_interrupt(&GPIOBinarySensorStore::gpio_intr, this, type); +} + void GPIOBinarySensor::setup() { - this->pin_->setup(); - this->publish_initial_state(this->pin_->digital_read()); + if (this->use_interrupt_ && !this->pin_->is_internal()) { + ESP_LOGD(TAG, "GPIO is not internal, falling back to polling mode"); + this->use_interrupt_ = false; + } + + if (this->use_interrupt_) { + auto *internal_pin = static_cast(this->pin_); + this->store_.setup(internal_pin, this->interrupt_type_, this); + this->publish_initial_state(this->store_.get_state()); + } else { + this->pin_->setup(); + this->publish_initial_state(this->pin_->digital_read()); + } } void GPIOBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "GPIO Binary Sensor", this); LOG_PIN(" Pin: ", this->pin_); + const char *mode = this->use_interrupt_ ? "interrupt" : "polling"; + ESP_LOGCONFIG(TAG, " Mode: %s", mode); + if (this->use_interrupt_) { + const char *interrupt_type; + switch (this->interrupt_type_) { + case gpio::INTERRUPT_RISING_EDGE: + interrupt_type = "RISING_EDGE"; + break; + case gpio::INTERRUPT_FALLING_EDGE: + interrupt_type = "FALLING_EDGE"; + break; + case gpio::INTERRUPT_ANY_EDGE: + interrupt_type = "ANY_EDGE"; + break; + default: + interrupt_type = "UNKNOWN"; + break; + } + ESP_LOGCONFIG(TAG, " Interrupt Type: %s", interrupt_type); + } } -void GPIOBinarySensor::loop() { this->publish_state(this->pin_->digital_read()); } +void GPIOBinarySensor::loop() { + if (this->use_interrupt_) { + if (this->store_.is_changed()) { + // Clear the flag immediately to minimize the window where we might miss changes + this->store_.clear_changed(); + // Read the state and publish it + // Note: If the ISR fires between clear_changed() and get_state(), that's fine - + // we'll process the new change on the next loop iteration + bool state = this->store_.get_state(); + this->publish_state(state); + } else { + // No changes, disable the loop until the next interrupt + this->disable_loop(); + } + } else { + this->publish_state(this->pin_->digital_read()); + } +} float GPIOBinarySensor::get_setup_priority() const { return setup_priority::HARDWARE; } diff --git a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h index 33a173fe2e..8cf52f540b 100644 --- a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h +++ b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h @@ -2,14 +2,51 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" +#include "esphome/core/helpers.h" #include "esphome/components/binary_sensor/binary_sensor.h" namespace esphome { namespace gpio { +// Store class for ISR data (no vtables, ISR-safe) +class GPIOBinarySensorStore { + public: + void setup(InternalGPIOPin *pin, gpio::InterruptType type, Component *component); + + static void gpio_intr(GPIOBinarySensorStore *arg); + + bool get_state() const { + // No lock needed: state_ is atomically updated by ISR + // Volatile ensures we read the latest value + return this->state_; + } + + bool is_changed() const { + // Simple read of volatile bool - no clearing here + return this->changed_; + } + + void clear_changed() { + // Separate method to clear the flag + this->changed_ = false; + } + + protected: + ISRInternalGPIOPin isr_pin_; + volatile bool state_{false}; + volatile bool last_state_{false}; + volatile bool changed_{false}; + Component *component_{nullptr}; // Pointer to the component for enable_loop_soon_any_context() +}; + class GPIOBinarySensor : public binary_sensor::BinarySensor, public Component { public: + // No destructor needed: ESPHome components are created at boot and live forever. + // Interrupts are only detached on reboot when memory is cleared anyway. + void set_pin(GPIOPin *pin) { pin_ = pin; } + void set_use_interrupt(bool use_interrupt) { use_interrupt_ = use_interrupt; } + void set_interrupt_type(gpio::InterruptType type) { interrupt_type_ = type; } // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) /// Setup pin @@ -22,6 +59,9 @@ class GPIOBinarySensor : public binary_sensor::BinarySensor, public Component { protected: GPIOPin *pin_; + bool use_interrupt_{true}; + gpio::InterruptType interrupt_type_{gpio::INTERRUPT_ANY_EDGE}; + GPIOBinarySensorStore store_; }; } // namespace gpio diff --git a/esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.h b/esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.h index 1987d33f37..aab881bd05 100644 --- a/esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.h +++ b/esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.h @@ -22,8 +22,6 @@ class GroveGasMultichannelV2Component : public PollingComponent, public i2c::I2C void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } - protected: enum ErrorCode { UNKNOWN, diff --git a/esphome/components/he60r/he60r.h b/esphome/components/he60r/he60r.h index e41e2203c1..02a2b44e66 100644 --- a/esphome/components/he60r/he60r.h +++ b/esphome/components/he60r/he60r.h @@ -13,7 +13,6 @@ class HE60rCover : public cover::Cover, public Component, public uart::UARTDevic void setup() override; void loop() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; }; void set_open_duration(uint32_t duration) { this->open_duration_ = duration; } void set_close_duration(uint32_t duration) { this->close_duration_ = duration; } diff --git a/esphome/components/heatpumpir/climate.py b/esphome/components/heatpumpir/climate.py index c0eb8db4b3..0f9f146ae9 100644 --- a/esphome/components/heatpumpir/climate.py +++ b/esphome/components/heatpumpir/climate.py @@ -70,6 +70,7 @@ PROTOCOLS = { "airway": Protocol.PROTOCOL_AIRWAY, "bgh_aud": Protocol.PROTOCOL_BGH_AUD, "panasonic_altdke": Protocol.PROTOCOL_PANASONIC_ALTDKE, + "philco_phs32": Protocol.PROTOCOL_PHILCO_PHS32, "vaillantvai8": Protocol.PROTOCOL_VAILLANTVAI8, "r51m": Protocol.PROTOCOL_R51M, } @@ -125,6 +126,6 @@ async def to_code(config): cg.add(var.set_max_temperature(config[CONF_MAX_TEMPERATURE])) cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE])) - cg.add_library("tonia/HeatpumpIR", "1.0.32") + cg.add_library("tonia/HeatpumpIR", "1.0.35") if CORE.is_libretiny: CORE.add_platformio_option("lib_ignore", "IRremoteESP8266") diff --git a/esphome/components/heatpumpir/heatpumpir.cpp b/esphome/components/heatpumpir/heatpumpir.cpp index d3476c6a71..f4d2ca6c1d 100644 --- a/esphome/components/heatpumpir/heatpumpir.cpp +++ b/esphome/components/heatpumpir/heatpumpir.cpp @@ -65,6 +65,7 @@ const std::map> PROTOCOL_CONSTRUCTOR_MAP {PROTOCOL_AIRWAY, []() { return new AIRWAYHeatpumpIR(); }}, // NOLINT {PROTOCOL_BGH_AUD, []() { return new BGHHeatpumpIR(); }}, // NOLINT {PROTOCOL_PANASONIC_ALTDKE, []() { return new PanasonicAltDKEHeatpumpIR(); }}, // NOLINT + {PROTOCOL_PHILCO_PHS32, []() { return new PhilcoPHS32HeatpumpIR(); }}, // NOLINT {PROTOCOL_VAILLANTVAI8, []() { return new VaillantHeatpumpIR(); }}, // NOLINT {PROTOCOL_R51M, []() { return new R51MHeatpumpIR(); }}, // NOLINT }; diff --git a/esphome/components/heatpumpir/heatpumpir.h b/esphome/components/heatpumpir/heatpumpir.h index b740d27af7..3e14c11861 100644 --- a/esphome/components/heatpumpir/heatpumpir.h +++ b/esphome/components/heatpumpir/heatpumpir.h @@ -65,6 +65,7 @@ enum Protocol { PROTOCOL_AIRWAY, PROTOCOL_BGH_AUD, PROTOCOL_PANASONIC_ALTDKE, + PROTOCOL_PHILCO_PHS32, PROTOCOL_VAILLANTVAI8, PROTOCOL_R51M, }; diff --git a/esphome/components/honeywellabp2_i2c/honeywellabp2.h b/esphome/components/honeywellabp2_i2c/honeywellabp2.h index bc81524ac2..274de847ac 100644 --- a/esphome/components/honeywellabp2_i2c/honeywellabp2.h +++ b/esphome/components/honeywellabp2_i2c/honeywellabp2.h @@ -18,7 +18,6 @@ class HONEYWELLABP2Sensor : public PollingComponent, public i2c::I2CDevice { void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; }; void loop() override; void update() override; - float get_setup_priority() const override { return setup_priority::DATA; }; void dump_config() override; void read_sensor_data(); diff --git a/esphome/components/host/__init__.py b/esphome/components/host/__init__.py index e275adafa9..d3dbcba6ed 100644 --- a/esphome/components/host/__init__.py +++ b/esphome/components/host/__init__.py @@ -41,6 +41,7 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): cg.add_build_flag("-DUSE_HOST") cg.add_define("USE_ESPHOME_HOST_MAC_ADDRESS", config[CONF_MAC_ADDRESS].parts) - cg.add_build_flag("-std=c++17") + cg.add_build_flag("-std=gnu++20") cg.add_define("ESPHOME_BOARD", "host") cg.add_platformio_option("platform", "platformio/native") + cg.add_platformio_option("lib_ldf_mode", "off") diff --git a/esphome/components/host/helpers.cpp b/esphome/components/host/helpers.cpp new file mode 100644 index 0000000000..fdad4f5cb6 --- /dev/null +++ b/esphome/components/host/helpers.cpp @@ -0,0 +1,57 @@ +#include "esphome/core/helpers.h" + +#ifdef USE_HOST + +#ifndef _WIN32 +#include +#include +#include +#endif +#include +#include +#include + +#include "esphome/core/defines.h" +#include "esphome/core/log.h" + +namespace esphome { + +static const char *const TAG = "helpers.host"; + +uint32_t random_uint32() { + std::random_device dev; + std::mt19937 rng(dev()); + std::uniform_int_distribution dist(0, std::numeric_limits::max()); + return dist(rng); +} + +bool random_bytes(uint8_t *data, size_t len) { + FILE *fp = fopen("/dev/urandom", "r"); + if (fp == nullptr) { + ESP_LOGW(TAG, "Could not open /dev/urandom, errno=%d", errno); + exit(1); + } + size_t read = fread(data, 1, len, fp); + if (read != len) { + ESP_LOGW(TAG, "Not enough data from /dev/urandom"); + exit(1); + } + fclose(fp); + return true; +} + +// Host platform uses std::mutex for proper thread synchronization +Mutex::Mutex() { handle_ = new std::mutex(); } +Mutex::~Mutex() { delete static_cast(handle_); } +void Mutex::lock() { static_cast(handle_)->lock(); } +bool Mutex::try_lock() { return static_cast(handle_)->try_lock(); } +void Mutex::unlock() { static_cast(handle_)->unlock(); } + +void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter) + static const uint8_t esphome_host_mac_address[6] = USE_ESPHOME_HOST_MAC_ADDRESS; + memcpy(mac, esphome_host_mac_address, sizeof(esphome_host_mac_address)); +} + +} // namespace esphome + +#endif // USE_HOST diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index ac13334118..0d32bc97c2 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -2,6 +2,7 @@ from esphome import automation import esphome.codegen as cg from esphome.components import esp32 from esphome.components.const import CONF_REQUEST_HEADERS +from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( CONF_ESP8266_DISABLE_SSL_SUPPORT, @@ -13,6 +14,7 @@ from esphome.const import ( CONF_URL, CONF_WATCHDOG_TIMEOUT, PLATFORM_HOST, + PlatformFramework, __version__, ) from esphome.core import CORE, Lambda @@ -175,7 +177,7 @@ async def to_code(config): not config.get(CONF_VERIFY_SSL), ) else: - cg.add_library("WiFiClientSecure", None) + cg.add_library("NetworkClientSecure", None) cg.add_library("HTTPClient", None) if CORE.is_esp8266: cg.add_library("ESP8266HTTPClient", None) @@ -319,3 +321,19 @@ async def http_request_action_to_code(config, action_id, template_arg, args): await automation.build_automation(trigger, [], conf) return var + + +FILTER_SOURCE_FILES = filter_source_files_from_platform( + { + "http_request_host.cpp": {PlatformFramework.HOST_NATIVE}, + "http_request_arduino.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP8266_ARDUINO, + PlatformFramework.RP2040_ARDUINO, + PlatformFramework.BK72XX_ARDUINO, + PlatformFramework.RTL87XX_ARDUINO, + PlatformFramework.LN882X_ARDUINO, + }, + "http_request_idf.cpp": {PlatformFramework.ESP32_IDF}, + } +) diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index a67b04eadc..95515f731a 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -239,7 +239,7 @@ template class HttpRequestSendAction : public Action { std::string response_body; if (this->capture_response_.value(x...)) { - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator allocator; uint8_t *buf = allocator.allocate(max_length); if (buf != nullptr) { size_t read_index = 0; diff --git a/esphome/components/http_request/http_request_arduino.h b/esphome/components/http_request/http_request_arduino.h index ac9ddffbb0..44744f8c78 100644 --- a/esphome/components/http_request/http_request_arduino.h +++ b/esphome/components/http_request/http_request_arduino.h @@ -6,6 +6,7 @@ #if defined(USE_ESP32) || defined(USE_RP2040) #include +#include #endif #ifdef USE_ESP8266 #include diff --git a/esphome/components/http_request/ota/ota_http_request.cpp b/esphome/components/http_request/ota/ota_http_request.cpp index 4c8d49dad5..4d9e868c74 100644 --- a/esphome/components/http_request/ota/ota_http_request.cpp +++ b/esphome/components/http_request/ota/ota_http_request.cpp @@ -258,7 +258,7 @@ bool OtaHttpRequestComponent::http_get_md5_() { } bool OtaHttpRequestComponent::validate_url_(const std::string &url) { - if ((url.length() < 8) || (url.find("http") != 0) || (url.find("://") == std::string::npos)) { + if ((url.length() < 8) || !url.starts_with("http") || (url.find("://") == std::string::npos)) { ESP_LOGE(TAG, "URL is invalid and/or must be prefixed with 'http://' or 'https://'"); return false; } diff --git a/esphome/components/http_request/update/http_request_update.cpp b/esphome/components/http_request/update/http_request_update.cpp index d683495ac6..202c7b88b2 100644 --- a/esphome/components/http_request/update/http_request_update.cpp +++ b/esphome/components/http_request/update/http_request_update.cpp @@ -50,15 +50,17 @@ void HttpRequestUpdate::update_task(void *params) { if (container == nullptr || container->status_code != HTTP_STATUS_OK) { std::string msg = str_sprintf("Failed to fetch manifest from %s", this_update->source_url_.c_str()); - this_update->status_set_error(msg.c_str()); + // Defer to main loop to avoid race condition on component_state_ read-modify-write + this_update->defer([this_update, msg]() { this_update->status_set_error(msg.c_str()); }); UPDATE_RETURN; } - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator allocator; uint8_t *data = allocator.allocate(container->content_length); if (data == nullptr) { - std::string msg = str_sprintf("Failed to allocate %d bytes for manifest", container->content_length); - this_update->status_set_error(msg.c_str()); + std::string msg = str_sprintf("Failed to allocate %zu bytes for manifest", container->content_length); + // Defer to main loop to avoid race condition on component_state_ read-modify-write + this_update->defer([this_update, msg]() { this_update->status_set_error(msg.c_str()); }); container->end(); UPDATE_RETURN; } @@ -120,7 +122,8 @@ void HttpRequestUpdate::update_task(void *params) { if (!valid) { std::string msg = str_sprintf("Failed to parse JSON from %s", this_update->source_url_.c_str()); - this_update->status_set_error(msg.c_str()); + // Defer to main loop to avoid race condition on component_state_ read-modify-write + this_update->defer([this_update, msg]() { this_update->status_set_error(msg.c_str()); }); UPDATE_RETURN; } @@ -147,18 +150,34 @@ void HttpRequestUpdate::update_task(void *params) { this_update->update_info_.current_version = current_version; } + bool trigger_update_available = false; + if (this_update->update_info_.latest_version.empty() || this_update->update_info_.latest_version == this_update->update_info_.current_version) { this_update->state_ = update::UPDATE_STATE_NO_UPDATE; } else { + if (this_update->state_ != update::UPDATE_STATE_AVAILABLE) { + trigger_update_available = true; + } this_update->state_ = update::UPDATE_STATE_AVAILABLE; } - this_update->update_info_.has_progress = false; - this_update->update_info_.progress = 0.0f; + // Defer to main loop to ensure thread-safe execution of: + // - status_clear_error() performs non-atomic read-modify-write on component_state_ + // - publish_state() triggers API callbacks that write to the shared protobuf buffer + // which can be corrupted if accessed concurrently from task and main loop threads + // - update_available trigger to ensure consistent state when the trigger fires + this_update->defer([this_update, trigger_update_available]() { + this_update->update_info_.has_progress = false; + this_update->update_info_.progress = 0.0f; - this_update->status_clear_error(); - this_update->publish_state(); + this_update->status_clear_error(); + this_update->publish_state(); + + if (trigger_update_available) { + this_update->get_update_available_trigger()->trigger(this_update->update_info_); + } + }); UPDATE_RETURN; } diff --git a/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp b/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp index 2d8381b60c..9d4680fdf4 100644 --- a/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp +++ b/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp @@ -159,12 +159,6 @@ void HydreonRGxxComponent::schedule_reboot_() { }); } -bool HydreonRGxxComponent::buffer_starts_with_(const std::string &prefix) { - return this->buffer_starts_with_(prefix.c_str()); -} - -bool HydreonRGxxComponent::buffer_starts_with_(const char *prefix) { return buffer_.rfind(prefix, 0) == 0; } - void HydreonRGxxComponent::process_line_() { ESP_LOGV(TAG, "Read from serial: %s", this->buffer_.substr(0, this->buffer_.size() - 2).c_str()); @@ -191,7 +185,7 @@ void HydreonRGxxComponent::process_line_() { ESP_LOGW(TAG, "Received EmSat!"); this->em_sat_ = true; } - if (this->buffer_starts_with_("PwrDays")) { + if (buffer_.starts_with("PwrDays")) { if (this->boot_count_ <= 0) { this->boot_count_ = 1; } else { @@ -220,7 +214,7 @@ void HydreonRGxxComponent::process_line_() { } return; } - if (this->buffer_starts_with_("SW")) { + if (buffer_.starts_with("SW")) { std::string::size_type majend = this->buffer_.find('.'); std::string::size_type endversion = this->buffer_.find(' ', 3); if (majend == std::string::npos || endversion == std::string::npos || majend > endversion) { @@ -282,7 +276,7 @@ void HydreonRGxxComponent::process_line_() { } } else { for (const auto *ignore : IGNORE_STRINGS) { - if (this->buffer_starts_with_(ignore)) { + if (buffer_.starts_with(ignore)) { ESP_LOGI(TAG, "Ignoring %s", this->buffer_.substr(0, this->buffer_.size() - 2).c_str()); return; } diff --git a/esphome/components/hydreon_rgxx/sensor.py b/esphome/components/hydreon_rgxx/sensor.py index f81703c087..6c9f1e2877 100644 --- a/esphome/components/hydreon_rgxx/sensor.py +++ b/esphome/components/hydreon_rgxx/sensor.py @@ -111,8 +111,8 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_MOISTURE): sensor.sensor_schema( unit_of_measurement=UNIT_INTENSITY, accuracy_decimals=0, - device_class=DEVICE_CLASS_PRECIPITATION_INTENSITY, state_class=STATE_CLASS_MEASUREMENT, + icon="mdi:weather-rainy", ), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index e20b7febeb..4172b23845 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -3,14 +3,13 @@ import logging from esphome import pins import esphome.codegen as cg from esphome.components import esp32 +from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( CONF_ADDRESS, CONF_FREQUENCY, CONF_I2C_ID, CONF_ID, - CONF_INPUT, - CONF_OUTPUT, CONF_SCAN, CONF_SCL, CONF_SDA, @@ -20,6 +19,7 @@ from esphome.const import ( PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040, + PlatformFramework, ) from esphome.core import CORE, coroutine_with_priority import esphome.final_validate as fv @@ -28,8 +28,9 @@ LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@esphome/core"] i2c_ns = cg.esphome_ns.namespace("i2c") I2CBus = i2c_ns.class_("I2CBus") -ArduinoI2CBus = i2c_ns.class_("ArduinoI2CBus", I2CBus, cg.Component) -IDFI2CBus = i2c_ns.class_("IDFI2CBus", I2CBus, cg.Component) +InternalI2CBus = i2c_ns.class_("InternalI2CBus", I2CBus) +ArduinoI2CBus = i2c_ns.class_("ArduinoI2CBus", InternalI2CBus, cg.Component) +IDFI2CBus = i2c_ns.class_("IDFI2CBus", InternalI2CBus, cg.Component) I2CDevice = i2c_ns.class_("I2CDevice") @@ -72,20 +73,15 @@ def validate_config(config): return config -pin_with_input_and_output_support = pins.internal_gpio_pin_number( - {CONF_OUTPUT: True, CONF_INPUT: True} -) - - CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): _bus_declare_type, - cv.Optional(CONF_SDA, default="SDA"): pin_with_input_and_output_support, + cv.Optional(CONF_SDA, default="SDA"): pins.internal_gpio_pin_number, cv.SplitDefault(CONF_SDA_PULLUP_ENABLED, esp32_idf=True): cv.All( cv.only_with_esp_idf, cv.boolean ), - cv.Optional(CONF_SCL, default="SCL"): pin_with_input_and_output_support, + cv.Optional(CONF_SCL, default="SCL"): pins.internal_gpio_pin_number, cv.SplitDefault(CONF_SCL_PULLUP_ENABLED, esp32_idf=True): cv.All( cv.only_with_esp_idf, cv.boolean ), @@ -104,6 +100,7 @@ CONFIG_SCHEMA = cv.All( @coroutine_with_priority(1.0) async def to_code(config): cg.add_global(i2c_ns.using) + cg.add_define("USE_I2C") var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) @@ -210,3 +207,18 @@ def final_validate_device_schema( {cv.Required(CONF_I2C_ID): fv.id_declaration_match_schema(hub_schema)}, extra=cv.ALLOW_EXTRA, ) + + +FILTER_SOURCE_FILES = filter_source_files_from_platform( + { + "i2c_bus_arduino.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP8266_ARDUINO, + PlatformFramework.RP2040_ARDUINO, + PlatformFramework.BK72XX_ARDUINO, + PlatformFramework.RTL87XX_ARDUINO, + PlatformFramework.LN882X_ARDUINO, + }, + "i2c_bus_esp_idf.cpp": {PlatformFramework.ESP32_IDF}, + } +) diff --git a/esphome/components/i2c/i2c_bus.h b/esphome/components/i2c/i2c_bus.h index fbfc88323e..5c1e15d814 100644 --- a/esphome/components/i2c/i2c_bus.h +++ b/esphome/components/i2c/i2c_bus.h @@ -1,6 +1,6 @@ #pragma once -#include #include +#include #include #include @@ -108,5 +108,12 @@ class I2CBus { bool scan_{false}; ///< Should we scan ? Can be set in the yaml }; +class InternalI2CBus : public I2CBus { + public: + /// @brief Returns the I2C port number. + /// @return the port number of the internal I2C bus + virtual int get_port() const = 0; +}; + } // namespace i2c } // namespace esphome diff --git a/esphome/components/i2c/i2c_bus_arduino.cpp b/esphome/components/i2c/i2c_bus_arduino.cpp index dca77e878d..a85df0a4cd 100644 --- a/esphome/components/i2c/i2c_bus_arduino.cpp +++ b/esphome/components/i2c/i2c_bus_arduino.cpp @@ -1,11 +1,11 @@ #ifdef USE_ARDUINO #include "i2c_bus_arduino.h" +#include +#include #include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include -#include namespace esphome { namespace i2c { @@ -23,6 +23,7 @@ void ArduinoI2CBus::setup() { } else { wire_ = new TwoWire(next_bus_num); // NOLINT(cppcoreguidelines-owning-memory) } + this->port_ = next_bus_num; next_bus_num++; #elif defined(USE_ESP8266) wire_ = new TwoWire(); // NOLINT(cppcoreguidelines-owning-memory) @@ -125,7 +126,7 @@ ErrorCode ArduinoI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt) size_t to_request = 0; for (size_t i = 0; i < cnt; i++) to_request += buffers[i].len; - size_t ret = wire_->requestFrom((int) address, (int) to_request, 1); + size_t ret = wire_->requestFrom(address, to_request, true); if (ret != to_request) { ESP_LOGVV(TAG, "RX %u from %02X failed with error %u", to_request, address, ret); return ERROR_TIMEOUT; diff --git a/esphome/components/i2c/i2c_bus_arduino.h b/esphome/components/i2c/i2c_bus_arduino.h index 6a670a2a05..7e6616cbce 100644 --- a/esphome/components/i2c/i2c_bus_arduino.h +++ b/esphome/components/i2c/i2c_bus_arduino.h @@ -2,9 +2,9 @@ #ifdef USE_ARDUINO -#include "i2c_bus.h" -#include "esphome/core/component.h" #include +#include "esphome/core/component.h" +#include "i2c_bus.h" namespace esphome { namespace i2c { @@ -15,7 +15,7 @@ enum RecoveryCode { RECOVERY_COMPLETED, }; -class ArduinoI2CBus : public I2CBus, public Component { +class ArduinoI2CBus : public InternalI2CBus, public Component { public: void setup() override; void dump_config() override; @@ -29,12 +29,15 @@ class ArduinoI2CBus : public I2CBus, public Component { void set_frequency(uint32_t frequency) { frequency_ = frequency; } void set_timeout(uint32_t timeout) { timeout_ = timeout; } + int get_port() const override { return this->port_; } + private: void recover_(); void set_pins_and_clock_(); RecoveryCode recovery_result_; protected: + int8_t port_{-1}; TwoWire *wire_; uint8_t sda_pin_; uint8_t scl_pin_; diff --git a/esphome/components/i2c/i2c_bus_esp_idf.h b/esphome/components/i2c/i2c_bus_esp_idf.h index afb4c2d22b..ee29578944 100644 --- a/esphome/components/i2c/i2c_bus_esp_idf.h +++ b/esphome/components/i2c/i2c_bus_esp_idf.h @@ -2,9 +2,9 @@ #ifdef USE_ESP_IDF -#include "i2c_bus.h" -#include "esphome/core/component.h" #include +#include "esphome/core/component.h" +#include "i2c_bus.h" namespace esphome { namespace i2c { @@ -15,7 +15,7 @@ enum RecoveryCode { RECOVERY_COMPLETED, }; -class IDFI2CBus : public I2CBus, public Component { +class IDFI2CBus : public InternalI2CBus, public Component { public: void setup() override; void dump_config() override; @@ -31,6 +31,8 @@ class IDFI2CBus : public I2CBus, public Component { void set_frequency(uint32_t frequency) { frequency_ = frequency; } void set_timeout(uint32_t timeout) { timeout_ = timeout; } + int get_port() const override { return static_cast(this->port_); } + private: void recover_(); RecoveryCode recovery_result_; diff --git a/esphome/components/i2c_device/i2c_device.h b/esphome/components/i2c_device/i2c_device.h index ab118e3e89..9944ca9204 100644 --- a/esphome/components/i2c_device/i2c_device.h +++ b/esphome/components/i2c_device/i2c_device.h @@ -9,7 +9,6 @@ namespace i2c_device { class I2CDeviceComponent : public Component, public i2c::I2CDevice { public: void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: }; diff --git a/esphome/components/i2s_audio/__init__.py b/esphome/components/i2s_audio/__init__.py index ef95fd0b41..9a2aa0362f 100644 --- a/esphome/components/i2s_audio/__init__.py +++ b/esphome/components/i2s_audio/__init__.py @@ -9,14 +9,7 @@ from esphome.components.esp32.const import ( VARIANT_ESP32S3, ) import esphome.config_validation as cv -from esphome.const import ( - CONF_BITS_PER_SAMPLE, - CONF_CHANNEL, - CONF_ID, - CONF_SAMPLE_RATE, - KEY_CORE, - KEY_FRAMEWORK_VERSION, -) +from esphome.const import CONF_BITS_PER_SAMPLE, CONF_CHANNEL, CONF_ID, CONF_SAMPLE_RATE from esphome.core import CORE from esphome.cpp_generator import MockObjClass import esphome.final_validate as fv @@ -250,8 +243,7 @@ def _final_validate(_): def use_legacy(): - framework_version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] - if CORE.using_esp_idf and framework_version >= cv.Version(5, 0, 0): + if CORE.using_esp_idf: if not _use_legacy_driver: return False return True diff --git a/esphome/components/i2s_audio/i2s_audio.cpp b/esphome/components/i2s_audio/i2s_audio.cpp index 0f2995b4bd..7f233516e6 100644 --- a/esphome/components/i2s_audio/i2s_audio.cpp +++ b/esphome/components/i2s_audio/i2s_audio.cpp @@ -9,15 +9,11 @@ namespace i2s_audio { static const char *const TAG = "i2s_audio"; -#if defined(USE_ESP_IDF) && (ESP_IDF_VERSION_MAJOR >= 5) -static const uint8_t I2S_NUM_MAX = SOC_I2S_NUM; // because IDF 5+ took this away :( -#endif - void I2SAudioComponent::setup() { ESP_LOGCONFIG(TAG, "Running setup"); static i2s_port_t next_port_num = I2S_NUM_0; - if (next_port_num >= I2S_NUM_MAX) { + if (next_port_num >= SOC_I2S_NUM) { ESP_LOGE(TAG, "Too many components"); this->mark_failed(); return; diff --git a/esphome/components/i2s_audio/media_player/__init__.py b/esphome/components/i2s_audio/media_player/__init__.py index f7ef134803..ad6665a5f5 100644 --- a/esphome/components/i2s_audio/media_player/__init__.py +++ b/esphome/components/i2s_audio/media_player/__init__.py @@ -114,7 +114,7 @@ async def to_code(config): cg.add(var.set_external_dac_channels(2 if config[CONF_MODE] == "stereo" else 1)) cg.add(var.set_i2s_comm_fmt_lsb(config[CONF_I2S_COMM_FMT] == "lsb")) - cg.add_library("WiFiClientSecure", None) + cg.add_library("NetworkClientSecure", None) cg.add_library("HTTPClient", None) - cg.add_library("esphome/ESP32-audioI2S", "2.2.0") + cg.add_library("esphome/ESP32-audioI2S", "2.3.0") cg.add_build_flag("-DAUDIO_NO_SD_FS") diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index 41da8a4642..1042a7ebee 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -484,7 +484,7 @@ bool I2SAudioSpeaker::send_esp_err_to_event_group_(esp_err_t err) { esp_err_t I2SAudioSpeaker::allocate_buffers_(size_t data_buffer_size, size_t ring_buffer_size) { if (this->data_buffer_ == nullptr) { // Allocate data buffer for temporarily storing audio from the ring buffer before writing to the I2S bus - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator allocator; this->data_buffer_ = allocator.allocate(data_buffer_size); } @@ -698,7 +698,7 @@ void I2SAudioSpeaker::delete_task_(size_t buffer_size) { this->audio_ring_buffer_.reset(); // Releases ownership of the shared_ptr if (this->data_buffer_ != nullptr) { - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator allocator; allocator.deallocate(this->data_buffer_, buffer_size); this->data_buffer_ = nullptr; } diff --git a/esphome/components/iaqcore/iaqcore.h b/esphome/components/iaqcore/iaqcore.h index f343c2a705..bb0bfcc754 100644 --- a/esphome/components/iaqcore/iaqcore.h +++ b/esphome/components/iaqcore/iaqcore.h @@ -16,8 +16,6 @@ class IAQCore : public PollingComponent, public i2c::I2CDevice { void update() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } - protected: sensor::Sensor *co2_{nullptr}; sensor::Sensor *tvoc_{nullptr}; diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index 5d593ac3d4..f6d8673a08 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -10,8 +10,10 @@ from PIL import Image, UnidentifiedImageError from esphome import core, external_files import esphome.codegen as cg +from esphome.components.const import CONF_BYTE_ORDER import esphome.config_validation as cv from esphome.const import ( + CONF_DEFAULTS, CONF_DITHER, CONF_FILE, CONF_ICON, @@ -38,6 +40,7 @@ CONF_OPAQUE = "opaque" CONF_CHROMA_KEY = "chroma_key" CONF_ALPHA_CHANNEL = "alpha_channel" CONF_INVERT_ALPHA = "invert_alpha" +CONF_IMAGES = "images" TRANSPARENCY_TYPES = ( CONF_OPAQUE, @@ -188,6 +191,10 @@ class ImageRGB565(ImageEncoder): dither, invert_alpha, ) + self.big_endian = True + + def set_big_endian(self, big_endian: bool) -> None: + self.big_endian = big_endian def convert(self, image, path): return image.convert("RGBA") @@ -205,10 +212,16 @@ class ImageRGB565(ImageEncoder): g = 1 b = 0 rgb = (r << 11) | (g << 5) | b - self.data[self.index] = rgb >> 8 - self.index += 1 - self.data[self.index] = rgb & 0xFF - self.index += 1 + if self.big_endian: + self.data[self.index] = rgb >> 8 + self.index += 1 + self.data[self.index] = rgb & 0xFF + self.index += 1 + else: + self.data[self.index] = rgb & 0xFF + self.index += 1 + self.data[self.index] = rgb >> 8 + self.index += 1 if self.transparency == CONF_ALPHA_CHANNEL: if self.invert_alpha: a ^= 0xFF @@ -364,7 +377,7 @@ def validate_file_shorthand(value): value = cv.string_strict(value) parts = value.strip().split(":") if len(parts) == 2 and parts[0] in MDI_SOURCES: - match = re.match(r"[a-zA-Z0-9\-]+", parts[1]) + match = re.match(r"^[a-zA-Z0-9\-]+$", parts[1]) if match is None: raise cv.Invalid(f"Could not parse mdi icon name from '{value}'.") return download_gh_svg(parts[1], parts[0]) @@ -434,20 +447,29 @@ def validate_type(image_types): def validate_settings(value): - type = value[CONF_TYPE] + """ + Validate the settings for a single image configuration. + """ + conf_type = value[CONF_TYPE] + type_class = IMAGE_TYPE[conf_type] transparency = value[CONF_TRANSPARENCY].lower() - allow_config = IMAGE_TYPE[type].allow_config - if transparency not in allow_config: + if transparency not in type_class.allow_config: raise cv.Invalid( - f"Image format '{type}' cannot have transparency: {transparency}" + f"Image format '{conf_type}' cannot have transparency: {transparency}" ) invert_alpha = value.get(CONF_INVERT_ALPHA, False) if ( invert_alpha and transparency != CONF_ALPHA_CHANNEL - and CONF_INVERT_ALPHA not in allow_config + and CONF_INVERT_ALPHA not in type_class.allow_config ): raise cv.Invalid("No alpha channel to invert") + if value.get(CONF_BYTE_ORDER) is not None and not callable( + getattr(type_class, "set_big_endian", None) + ): + raise cv.Invalid( + f"Image format '{conf_type}' does not support byte order configuration" + ) if file := value.get(CONF_FILE): file = Path(file) if is_svg_file(file): @@ -456,31 +478,82 @@ def validate_settings(value): try: Image.open(file) except UnidentifiedImageError as exc: - raise cv.Invalid(f"File can't be opened as image: {file}") from exc + raise cv.Invalid( + f"File can't be opened as image: {file.absolute()}" + ) from exc return value +IMAGE_ID_SCHEMA = { + cv.Required(CONF_ID): cv.declare_id(Image_), + cv.Required(CONF_FILE): cv.Any(validate_file_shorthand, TYPED_FILE_SCHEMA), + cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), +} + + +OPTIONS_SCHEMA = { + cv.Optional(CONF_RESIZE): cv.dimensions, + cv.Optional(CONF_DITHER, default="NONE"): cv.one_of( + "NONE", "FLOYDSTEINBERG", upper=True + ), + cv.Optional(CONF_INVERT_ALPHA, default=False): cv.boolean, + cv.Optional(CONF_BYTE_ORDER): cv.one_of("BIG_ENDIAN", "LITTLE_ENDIAN", upper=True), + cv.Optional(CONF_TRANSPARENCY, default=CONF_OPAQUE): validate_transparency(), + cv.Optional(CONF_TYPE): validate_type(IMAGE_TYPE), +} + +OPTIONS = [key.schema for key in OPTIONS_SCHEMA] + +# image schema with no defaults, used with `CONF_IMAGES` in the config +IMAGE_SCHEMA_NO_DEFAULTS = { + **IMAGE_ID_SCHEMA, + **{cv.Optional(key): OPTIONS_SCHEMA[key] for key in OPTIONS}, +} + BASE_SCHEMA = cv.Schema( { - cv.Required(CONF_ID): cv.declare_id(Image_), - cv.Required(CONF_FILE): cv.Any(validate_file_shorthand, TYPED_FILE_SCHEMA), - cv.Optional(CONF_RESIZE): cv.dimensions, - cv.Optional(CONF_DITHER, default="NONE"): cv.one_of( - "NONE", "FLOYDSTEINBERG", upper=True - ), - cv.Optional(CONF_INVERT_ALPHA, default=False): cv.boolean, - cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), + **IMAGE_ID_SCHEMA, + **OPTIONS_SCHEMA, } ).add_extra(validate_settings) IMAGE_SCHEMA = BASE_SCHEMA.extend( { cv.Required(CONF_TYPE): validate_type(IMAGE_TYPE), - cv.Optional(CONF_TRANSPARENCY, default=CONF_OPAQUE): validate_transparency(), } ) +def validate_defaults(value): + """ + Validate the options for images with defaults + """ + defaults = value[CONF_DEFAULTS] + result = [] + for index, image in enumerate(value[CONF_IMAGES]): + type = image.get(CONF_TYPE, defaults.get(CONF_TYPE)) + if type is None: + raise cv.Invalid( + "Type is required either in the image config or in the defaults", + path=[CONF_IMAGES, index], + ) + type_class = IMAGE_TYPE[type] + # A default byte order should be simply ignored if the type does not support it + available_options = [*OPTIONS] + if ( + not callable(getattr(type_class, "set_big_endian", None)) + and CONF_BYTE_ORDER not in image + ): + available_options.remove(CONF_BYTE_ORDER) + config = { + **{key: image.get(key, defaults.get(key)) for key in available_options}, + **{key.schema: image[key.schema] for key in IMAGE_ID_SCHEMA}, + } + validate_settings(config) + result.append(config) + return result + + def typed_image_schema(image_type): """ Construct a schema for a specific image type, allowing transparency options @@ -523,10 +596,33 @@ def typed_image_schema(image_type): # The config schema can be a (possibly empty) single list of images, # or a dictionary of image types each with a list of images -CONFIG_SCHEMA = cv.Any( - cv.Schema({cv.Optional(t.lower()): typed_image_schema(t) for t in IMAGE_TYPE}), - cv.ensure_list(IMAGE_SCHEMA), -) +# or a dictionary with keys `defaults:` and `images:` + + +def _config_schema(config): + if isinstance(config, list): + return cv.Schema([IMAGE_SCHEMA])(config) + if not isinstance(config, dict): + raise cv.Invalid( + "Badly formed image configuration, expected a list or a dictionary" + ) + if CONF_DEFAULTS in config or CONF_IMAGES in config: + return validate_defaults( + cv.Schema( + { + cv.Required(CONF_DEFAULTS): OPTIONS_SCHEMA, + cv.Required(CONF_IMAGES): cv.ensure_list(IMAGE_SCHEMA_NO_DEFAULTS), + } + )(config) + ) + if CONF_ID in config or CONF_FILE in config: + return cv.ensure_list(IMAGE_SCHEMA)([config]) + return cv.Schema( + {cv.Optional(t.lower()): typed_image_schema(t) for t in IMAGE_TYPE} + )(config) + + +CONFIG_SCHEMA = _config_schema async def write_image(config, all_frames=False): @@ -585,6 +681,9 @@ async def write_image(config, all_frames=False): total_rows = height * frame_count encoder = IMAGE_TYPE[type](width, total_rows, transparency, dither, invert_alpha) + if byte_order := config.get(CONF_BYTE_ORDER): + # Check for valid type has already been done in validate_settings + encoder.set_big_endian(byte_order == "BIG_ENDIAN") for frame_index in range(frame_count): image.seek(frame_index) pixels = encoder.convert(image.resize((width, height)), path).getdata() diff --git a/esphome/components/improv_serial/improv_serial_component.cpp b/esphome/components/improv_serial/improv_serial_component.cpp index c3a0f2eacc..ae4927828b 100644 --- a/esphome/components/improv_serial/improv_serial_component.cpp +++ b/esphome/components/improv_serial/improv_serial_component.cpp @@ -59,11 +59,7 @@ optional ImprovSerialComponent::read_byte_() { break; #if defined(USE_LOGGER_USB_CDC) && defined(CONFIG_ESP_CONSOLE_USB_CDC) case logger::UART_SELECTION_USB_CDC: -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) if (esp_usb_console_available_for_read()) { -#else - if (esp_usb_console_read_available()) { -#endif esp_usb_console_read_buf((char *) &data, 1); byte = data; } diff --git a/esphome/components/ina219/ina219.cpp b/esphome/components/ina219/ina219.cpp index 8d5271fa84..52a3b1e067 100644 --- a/esphome/components/ina219/ina219.cpp +++ b/esphome/components/ina219/ina219.cpp @@ -129,6 +129,13 @@ void INA219Component::setup() { } } +void INA219Component::on_powerdown() { + // Mode = 0 -> power down + if (!this->write_byte_16(INA219_REGISTER_CONFIG, 0)) { + ESP_LOGE(TAG, "powerdown error"); + } +} + void INA219Component::dump_config() { ESP_LOGCONFIG(TAG, "INA219:"); LOG_I2C_DEVICE(this); diff --git a/esphome/components/ina219/ina219.h b/esphome/components/ina219/ina219.h index a6c0f2bc4c..115fa886e0 100644 --- a/esphome/components/ina219/ina219.h +++ b/esphome/components/ina219/ina219.h @@ -15,6 +15,7 @@ class INA219Component : public PollingComponent, public i2c::I2CDevice { void dump_config() override; float get_setup_priority() const override; void update() override; + void on_powerdown() override; void set_shunt_resistance_ohm(float shunt_resistance_ohm) { shunt_resistance_ohm_ = shunt_resistance_ohm; } void set_max_current_a(float max_current_a) { max_current_a_ = max_current_a; } diff --git a/esphome/components/ina260/ina260.h b/esphome/components/ina260/ina260.h index 8bad1cba6d..6cbc157cf3 100644 --- a/esphome/components/ina260/ina260.h +++ b/esphome/components/ina260/ina260.h @@ -13,8 +13,6 @@ class INA260Component : public PollingComponent, public i2c::I2CDevice { void dump_config() override; void update() override; - float get_setup_priority() const override { return setup_priority::DATA; } - void set_bus_voltage_sensor(sensor::Sensor *bus_voltage_sensor) { this->bus_voltage_sensor_ = bus_voltage_sensor; } void set_current_sensor(sensor::Sensor *current_sensor) { this->current_sensor_ = current_sensor; } void set_power_sensor(sensor::Sensor *power_sensor) { this->power_sensor_ = power_sensor; } diff --git a/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.h b/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.h index bdca2d0cac..cd2ea99717 100644 --- a/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.h +++ b/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.h @@ -16,7 +16,6 @@ class InkbirdIbstH1Mini : public Component, public esp32_ble_tracker::ESPBTDevic bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } void set_external_temperature(sensor::Sensor *external_temperature) { external_temperature_ = external_temperature; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } diff --git a/esphome/components/inkplate6/display.py b/esphome/components/inkplate6/display.py index a7d31c0131..063fc8b0aa 100644 --- a/esphome/components/inkplate6/display.py +++ b/esphome/components/inkplate6/display.py @@ -1,6 +1,7 @@ from esphome import pins import esphome.codegen as cg from esphome.components import display, i2c +from esphome.components.esp32 import CONF_CPU_FREQUENCY import esphome.config_validation as cv from esphome.const import ( CONF_FULL_UPDATE_EVERY, @@ -13,7 +14,9 @@ from esphome.const import ( CONF_PAGES, CONF_TRANSFORM, CONF_WAKEUP_PIN, + PLATFORM_ESP32, ) +import esphome.final_validate as fv DEPENDENCIES = ["i2c", "esp32"] AUTO_LOAD = ["psram"] @@ -120,6 +123,18 @@ CONFIG_SCHEMA = cv.All( ) +def _validate_cpu_frequency(config): + esp32_config = fv.full_config.get()[PLATFORM_ESP32] + if esp32_config[CONF_CPU_FREQUENCY] != "240MHZ": + raise cv.Invalid( + "Inkplate requires 240MHz CPU frequency (set in esp32 component)" + ) + return config + + +FINAL_VALIDATE_SCHEMA = _validate_cpu_frequency + + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) diff --git a/esphome/components/inkplate6/inkplate.cpp b/esphome/components/inkplate6/inkplate.cpp index 247aa35ead..b3d0b87e83 100644 --- a/esphome/components/inkplate6/inkplate.cpp +++ b/esphome/components/inkplate6/inkplate.cpp @@ -57,8 +57,8 @@ void Inkplate6::setup() { * Allocate buffers. May be called after setup to re-initialise if e.g. greyscale is changed. */ void Inkplate6::initialize_() { - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); - ExternalRAMAllocator allocator32(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator allocator; + RAMAllocator allocator32; uint32_t buffer_size = this->get_buffer_length_(); if (buffer_size == 0) return; diff --git a/esphome/components/integration/integration_sensor.h b/esphome/components/integration/integration_sensor.h index e84d7a8ed1..d9f2f5e50f 100644 --- a/esphome/components/integration/integration_sensor.h +++ b/esphome/components/integration/integration_sensor.h @@ -27,7 +27,6 @@ class IntegrationSensor : public sensor::Sensor, public Component { public: void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_sensor(Sensor *sensor) { sensor_ = sensor; } void set_time(IntegrationSensorTime time) { time_ = time; } void set_method(IntegrationMethod method) { method_ = method; } diff --git a/esphome/components/internal_temperature/internal_temperature.cpp b/esphome/components/internal_temperature/internal_temperature.cpp index f503927d8e..85844647f2 100644 --- a/esphome/components/internal_temperature/internal_temperature.cpp +++ b/esphome/components/internal_temperature/internal_temperature.cpp @@ -10,11 +10,7 @@ uint8_t temprature_sens_read(); #elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || \ defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32H2) || \ defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32P4) -#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) -#include "driver/temp_sensor.h" -#else #include "driver/temperature_sensor.h" -#endif // ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) #endif // USE_ESP32_VARIANT #endif // USE_ESP32 #ifdef USE_RP2040 @@ -31,12 +27,11 @@ namespace internal_temperature { static const char *const TAG = "internal_temperature"; #ifdef USE_ESP32 -#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)) && \ - (defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32S2) || \ - defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32C2) || \ - defined(USE_ESP32_VARIANT_ESP32P4)) +#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32S2) || \ + defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32C2) || \ + defined(USE_ESP32_VARIANT_ESP32P4) static temperature_sensor_handle_t tsensNew = NULL; -#endif // ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) && USE_ESP32_VARIANT +#endif // USE_ESP32_VARIANT #endif // USE_ESP32 void InternalTemperatureSensor::update() { @@ -51,24 +46,11 @@ void InternalTemperatureSensor::update() { #elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || \ defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32H2) || \ defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32P4) -#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) - temp_sensor_config_t tsens = TSENS_CONFIG_DEFAULT(); - temp_sensor_set_config(tsens); - temp_sensor_start(); -#if defined(USE_ESP32_VARIANT_ESP32S3) && (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4, 4, 3)) -#error \ - "ESP32-S3 internal temperature sensor requires ESP IDF V4.4.3 or higher. See https://github.com/esphome/issues/issues/4271" -#endif - esp_err_t result = temp_sensor_read_celsius(&temperature); - temp_sensor_stop(); - success = (result == ESP_OK); -#else esp_err_t result = temperature_sensor_get_celsius(tsensNew, &temperature); success = (result == ESP_OK); if (!success) { ESP_LOGE(TAG, "Reading failed (%d)", result); } -#endif // ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) #endif // USE_ESP32_VARIANT #endif // USE_ESP32 #ifdef USE_RP2040 @@ -99,10 +81,9 @@ void InternalTemperatureSensor::update() { void InternalTemperatureSensor::setup() { #ifdef USE_ESP32 -#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)) && \ - (defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32S2) || \ - defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32C2) || \ - defined(USE_ESP32_VARIANT_ESP32P4)) +#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32S2) || \ + defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32C2) || \ + defined(USE_ESP32_VARIANT_ESP32P4) ESP_LOGCONFIG(TAG, "Running setup"); temperature_sensor_config_t tsens_config = TEMPERATURE_SENSOR_CONFIG_DEFAULT(-10, 80); @@ -120,7 +101,7 @@ void InternalTemperatureSensor::setup() { this->mark_failed(); return; } -#endif // ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) && USE_ESP32_VARIANT +#endif // USE_ESP32_VARIANT #endif // USE_ESP32 } diff --git a/esphome/components/internal_temperature/sensor.py b/esphome/components/internal_temperature/sensor.py index 9bfa3739c8..93b98a30f4 100644 --- a/esphome/components/internal_temperature/sensor.py +++ b/esphome/components/internal_temperature/sensor.py @@ -1,46 +1,21 @@ import esphome.codegen as cg from esphome.components import sensor -from esphome.components.esp32 import get_esp32_variant -from esphome.components.esp32.const import VARIANT_ESP32S3 import esphome.config_validation as cv from esphome.const import ( DEVICE_CLASS_TEMPERATURE, ENTITY_CATEGORY_DIAGNOSTIC, - KEY_CORE, - KEY_FRAMEWORK_VERSION, PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_RP2040, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, ) -from esphome.core import CORE internal_temperature_ns = cg.esphome_ns.namespace("internal_temperature") InternalTemperatureSensor = internal_temperature_ns.class_( "InternalTemperatureSensor", sensor.Sensor, cg.PollingComponent ) - -def validate_config(config): - if CORE.is_esp32: - variant = get_esp32_variant() - if variant == VARIANT_ESP32S3: - if CORE.using_arduino and CORE.data[KEY_CORE][ - KEY_FRAMEWORK_VERSION - ] < cv.Version(2, 0, 6): - raise cv.Invalid( - "ESP32-S3 Internal Temperature Sensor requires framework version 2.0.6 or higher. See ." - ) - if CORE.using_esp_idf and CORE.data[KEY_CORE][ - KEY_FRAMEWORK_VERSION - ] < cv.Version(4, 4, 3): - raise cv.Invalid( - "ESP32-S3 Internal Temperature Sensor requires framework version 4.4.3 or higher. See ." - ) - return config - - CONFIG_SCHEMA = cv.All( sensor.sensor_schema( InternalTemperatureSensor, @@ -51,7 +26,6 @@ CONFIG_SCHEMA = cv.All( entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ).extend(cv.polling_component_schema("60s")), cv.only_on([PLATFORM_ESP32, PLATFORM_RP2040, PLATFORM_BK72XX]), - validate_config, ) diff --git a/esphome/components/interval/interval.h b/esphome/components/interval/interval.h index 5b8bc3081f..8f904b104d 100644 --- a/esphome/components/interval/interval.h +++ b/esphome/components/interval/interval.h @@ -23,8 +23,6 @@ class IntervalTrigger : public Trigger<>, public PollingComponent { void set_startup_delay(const uint32_t startup_delay) { this->startup_delay_ = startup_delay; } - float get_setup_priority() const override { return setup_priority::DATA; } - protected: uint32_t startup_delay_{0}; bool started_{false}; diff --git a/esphome/components/ld2410/button/__init__.py b/esphome/components/ld2410/button/__init__.py index 4cb50d707b..1cd56082c3 100644 --- a/esphome/components/ld2410/button/__init__.py +++ b/esphome/components/ld2410/button/__init__.py @@ -14,8 +14,8 @@ from esphome.const import ( from .. import CONF_LD2410_ID, LD2410Component, ld2410_ns +FactoryResetButton = ld2410_ns.class_("FactoryResetButton", button.Button) QueryButton = ld2410_ns.class_("QueryButton", button.Button) -ResetButton = ld2410_ns.class_("ResetButton", button.Button) RestartButton = ld2410_ns.class_("RestartButton", button.Button) CONF_QUERY_PARAMS = "query_params" @@ -23,7 +23,7 @@ CONF_QUERY_PARAMS = "query_params" CONFIG_SCHEMA = { cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component), cv.Optional(CONF_FACTORY_RESET): button.button_schema( - ResetButton, + FactoryResetButton, device_class=DEVICE_CLASS_RESTART, entity_category=ENTITY_CATEGORY_CONFIG, icon=ICON_RESTART_ALERT, @@ -47,7 +47,7 @@ async def to_code(config): if factory_reset_config := config.get(CONF_FACTORY_RESET): b = await button.new_button(factory_reset_config) await cg.register_parented(b, config[CONF_LD2410_ID]) - cg.add(ld2410_component.set_reset_button(b)) + cg.add(ld2410_component.set_factory_reset_button(b)) if restart_config := config.get(CONF_RESTART): b = await button.new_button(restart_config) await cg.register_parented(b, config[CONF_LD2410_ID]) diff --git a/esphome/components/ld2410/button/factory_reset_button.cpp b/esphome/components/ld2410/button/factory_reset_button.cpp new file mode 100644 index 0000000000..a848b02a9d --- /dev/null +++ b/esphome/components/ld2410/button/factory_reset_button.cpp @@ -0,0 +1,9 @@ +#include "factory_reset_button.h" + +namespace esphome { +namespace ld2410 { + +void FactoryResetButton::press_action() { this->parent_->factory_reset(); } + +} // namespace ld2410 +} // namespace esphome diff --git a/esphome/components/ld2410/button/reset_button.h b/esphome/components/ld2410/button/factory_reset_button.h similarity index 65% rename from esphome/components/ld2410/button/reset_button.h rename to esphome/components/ld2410/button/factory_reset_button.h index 78dd92c9f5..45bf979033 100644 --- a/esphome/components/ld2410/button/reset_button.h +++ b/esphome/components/ld2410/button/factory_reset_button.h @@ -6,9 +6,9 @@ namespace esphome { namespace ld2410 { -class ResetButton : public button::Button, public Parented { +class FactoryResetButton : public button::Button, public Parented { public: - ResetButton() = default; + FactoryResetButton() = default; protected: void press_action() override; diff --git a/esphome/components/ld2410/button/reset_button.cpp b/esphome/components/ld2410/button/reset_button.cpp deleted file mode 100644 index f16c5faa79..0000000000 --- a/esphome/components/ld2410/button/reset_button.cpp +++ /dev/null @@ -1,9 +0,0 @@ -#include "reset_button.h" - -namespace esphome { -namespace ld2410 { - -void ResetButton::press_action() { this->parent_->factory_reset(); } - -} // namespace ld2410 -} // namespace esphome diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index d7007ae0bd..375d1088e8 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -8,6 +8,9 @@ #include "esphome/components/sensor/sensor.h" #endif +#include "esphome/core/application.h" + +#define CHECK_BIT(var, pos) (((var) >> (pos)) & 1) #define highbyte(val) (uint8_t)((val) >> 8) #define lowbyte(val) (uint8_t)((val) &0xff) @@ -15,68 +18,244 @@ namespace esphome { namespace ld2410 { static const char *const TAG = "ld2410"; +static const char *const UNKNOWN_MAC = "unknown"; +static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X"; -LD2410Component::LD2410Component() {} +enum BaudRate : uint8_t { + BAUD_RATE_9600 = 1, + BAUD_RATE_19200 = 2, + BAUD_RATE_38400 = 3, + BAUD_RATE_57600 = 4, + BAUD_RATE_115200 = 5, + BAUD_RATE_230400 = 6, + BAUD_RATE_256000 = 7, + BAUD_RATE_460800 = 8, +}; + +enum DistanceResolution : uint8_t { + DISTANCE_RESOLUTION_0_2 = 0x01, + DISTANCE_RESOLUTION_0_75 = 0x00, +}; + +enum LightFunction : uint8_t { + LIGHT_FUNCTION_OFF = 0x00, + LIGHT_FUNCTION_BELOW = 0x01, + LIGHT_FUNCTION_ABOVE = 0x02, +}; + +enum OutPinLevel : uint8_t { + OUT_PIN_LEVEL_LOW = 0x00, + OUT_PIN_LEVEL_HIGH = 0x01, +}; + +enum PeriodicData : uint8_t { + DATA_TYPES = 6, + TARGET_STATES = 8, + MOVING_TARGET_LOW = 9, + MOVING_TARGET_HIGH = 10, + MOVING_ENERGY = 11, + STILL_TARGET_LOW = 12, + STILL_TARGET_HIGH = 13, + STILL_ENERGY = 14, + DETECT_DISTANCE_LOW = 15, + DETECT_DISTANCE_HIGH = 16, + MOVING_SENSOR_START = 19, + STILL_SENSOR_START = 28, + LIGHT_SENSOR = 37, + OUT_PIN_SENSOR = 38, +}; + +enum PeriodicDataValue : uint8_t { + HEADER = 0xAA, + FOOTER = 0x55, + CHECK = 0x00, +}; + +enum AckData : uint8_t { + COMMAND = 6, + COMMAND_STATUS = 7, +}; + +// Memory-efficient lookup tables +struct StringToUint8 { + const char *str; + const uint8_t value; +}; + +struct Uint8ToString { + const uint8_t value; + const char *str; +}; + +constexpr StringToUint8 BAUD_RATES_BY_STR[] = { + {"9600", BAUD_RATE_9600}, {"19200", BAUD_RATE_19200}, {"38400", BAUD_RATE_38400}, + {"57600", BAUD_RATE_57600}, {"115200", BAUD_RATE_115200}, {"230400", BAUD_RATE_230400}, + {"256000", BAUD_RATE_256000}, {"460800", BAUD_RATE_460800}, +}; + +constexpr StringToUint8 DISTANCE_RESOLUTIONS_BY_STR[] = { + {"0.2m", DISTANCE_RESOLUTION_0_2}, + {"0.75m", DISTANCE_RESOLUTION_0_75}, +}; + +constexpr Uint8ToString DISTANCE_RESOLUTIONS_BY_UINT[] = { + {DISTANCE_RESOLUTION_0_2, "0.2m"}, + {DISTANCE_RESOLUTION_0_75, "0.75m"}, +}; + +constexpr StringToUint8 LIGHT_FUNCTIONS_BY_STR[] = { + {"off", LIGHT_FUNCTION_OFF}, + {"below", LIGHT_FUNCTION_BELOW}, + {"above", LIGHT_FUNCTION_ABOVE}, +}; + +constexpr Uint8ToString LIGHT_FUNCTIONS_BY_UINT[] = { + {LIGHT_FUNCTION_OFF, "off"}, + {LIGHT_FUNCTION_BELOW, "below"}, + {LIGHT_FUNCTION_ABOVE, "above"}, +}; + +constexpr StringToUint8 OUT_PIN_LEVELS_BY_STR[] = { + {"low", OUT_PIN_LEVEL_LOW}, + {"high", OUT_PIN_LEVEL_HIGH}, +}; + +constexpr Uint8ToString OUT_PIN_LEVELS_BY_UINT[] = { + {OUT_PIN_LEVEL_LOW, "low"}, + {OUT_PIN_LEVEL_HIGH, "high"}, +}; + +// Helper functions for lookups +template uint8_t find_uint8(const StringToUint8 (&arr)[N], const std::string &str) { + for (const auto &entry : arr) { + if (str == entry.str) + return entry.value; + } + return 0xFF; // Not found +} + +template const char *find_str(const Uint8ToString (&arr)[N], uint8_t value) { + for (const auto &entry : arr) { + if (value == entry.value) + return entry.str; + } + return ""; // Not found +} + +// Commands +static constexpr uint8_t CMD_ENABLE_CONF = 0xFF; +static constexpr uint8_t CMD_DISABLE_CONF = 0xFE; +static constexpr uint8_t CMD_ENABLE_ENG = 0x62; +static constexpr uint8_t CMD_DISABLE_ENG = 0x63; +static constexpr uint8_t CMD_MAXDIST_DURATION = 0x60; +static constexpr uint8_t CMD_QUERY = 0x61; +static constexpr uint8_t CMD_GATE_SENS = 0x64; +static constexpr uint8_t CMD_QUERY_VERSION = 0xA0; +static constexpr uint8_t CMD_QUERY_DISTANCE_RESOLUTION = 0xAB; +static constexpr uint8_t CMD_SET_DISTANCE_RESOLUTION = 0xAA; +static constexpr uint8_t CMD_QUERY_LIGHT_CONTROL = 0xAE; +static constexpr uint8_t CMD_SET_LIGHT_CONTROL = 0xAD; +static constexpr uint8_t CMD_SET_BAUD_RATE = 0xA1; +static constexpr uint8_t CMD_BT_PASSWORD = 0xA9; +static constexpr uint8_t CMD_QUERY_MAC_ADDRESS = 0xA5; +static constexpr uint8_t CMD_RESET = 0xA2; +static constexpr uint8_t CMD_RESTART = 0xA3; +static constexpr uint8_t CMD_BLUETOOTH = 0xA4; +// Commands values +static constexpr uint8_t CMD_MAX_MOVE_VALUE = 0x00; +static constexpr uint8_t CMD_MAX_STILL_VALUE = 0x01; +static constexpr uint8_t CMD_DURATION_VALUE = 0x02; +// Header & Footer size +static constexpr uint8_t HEADER_FOOTER_SIZE = 4; +// Command Header & Footer +static constexpr uint8_t CMD_FRAME_HEADER[HEADER_FOOTER_SIZE] = {0xFD, 0xFC, 0xFB, 0xFA}; +static constexpr uint8_t CMD_FRAME_FOOTER[HEADER_FOOTER_SIZE] = {0x04, 0x03, 0x02, 0x01}; +// Data Header & Footer +static constexpr uint8_t DATA_FRAME_HEADER[HEADER_FOOTER_SIZE] = {0xF4, 0xF3, 0xF2, 0xF1}; +static constexpr uint8_t DATA_FRAME_FOOTER[HEADER_FOOTER_SIZE] = {0xF8, 0xF7, 0xF6, 0xF5}; +// MAC address the module uses when Bluetooth is disabled +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 bool validate_header_footer(const uint8_t *header_footer, const uint8_t *buffer) { + for (uint8_t i = 0; i < HEADER_FOOTER_SIZE; i++) { + if (header_footer[i] != buffer[i]) { + return false; // Mismatch in header/footer + } + } + return true; // Valid header/footer +} void LD2410Component::dump_config() { - ESP_LOGCONFIG(TAG, "LD2410:"); + std::string mac_str = + mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC; + std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5], + this->version_[4], this->version_[3], this->version_[2]); + ESP_LOGCONFIG(TAG, + "LD2410:\n" + " Firmware version: %s\n" + " MAC address: %s\n" + " Throttle: %u ms", + version.c_str(), mac_str.c_str(), this->throttle_); #ifdef USE_BINARY_SENSOR - LOG_BINARY_SENSOR(" ", "TargetBinarySensor", this->target_binary_sensor_); - LOG_BINARY_SENSOR(" ", "MovingTargetBinarySensor", this->moving_target_binary_sensor_); - LOG_BINARY_SENSOR(" ", "StillTargetBinarySensor", this->still_target_binary_sensor_); - LOG_BINARY_SENSOR(" ", "OutPinPresenceStatusBinarySensor", this->out_pin_presence_status_binary_sensor_); -#endif -#ifdef USE_SWITCH - LOG_SWITCH(" ", "EngineeringModeSwitch", this->engineering_mode_switch_); - LOG_SWITCH(" ", "BluetoothSwitch", this->bluetooth_switch_); -#endif -#ifdef USE_BUTTON - LOG_BUTTON(" ", "ResetButton", this->reset_button_); - LOG_BUTTON(" ", "RestartButton", this->restart_button_); - LOG_BUTTON(" ", "QueryButton", this->query_button_); + ESP_LOGCONFIG(TAG, "Binary Sensors:"); + LOG_BINARY_SENSOR(" ", "Target", this->target_binary_sensor_); + LOG_BINARY_SENSOR(" ", "MovingTarget", this->moving_target_binary_sensor_); + LOG_BINARY_SENSOR(" ", "StillTarget", this->still_target_binary_sensor_); + LOG_BINARY_SENSOR(" ", "OutPinPresenceStatus", this->out_pin_presence_status_binary_sensor_); #endif #ifdef USE_SENSOR - LOG_SENSOR(" ", "LightSensor", this->light_sensor_); - LOG_SENSOR(" ", "MovingTargetDistanceSensor", this->moving_target_distance_sensor_); - LOG_SENSOR(" ", "StillTargetDistanceSensor", this->still_target_distance_sensor_); - LOG_SENSOR(" ", "MovingTargetEnergySensor", this->moving_target_energy_sensor_); - LOG_SENSOR(" ", "StillTargetEnergySensor", this->still_target_energy_sensor_); - LOG_SENSOR(" ", "DetectionDistanceSensor", this->detection_distance_sensor_); - for (sensor::Sensor *s : this->gate_still_sensors_) { - LOG_SENSOR(" ", "NthGateStillSesnsor", s); - } + ESP_LOGCONFIG(TAG, "Sensors:"); + LOG_SENSOR(" ", "Light", this->light_sensor_); + LOG_SENSOR(" ", "DetectionDistance", this->detection_distance_sensor_); + LOG_SENSOR(" ", "MovingTargetDistance", this->moving_target_distance_sensor_); + LOG_SENSOR(" ", "MovingTargetEnergy", this->moving_target_energy_sensor_); + LOG_SENSOR(" ", "StillTargetDistance", this->still_target_distance_sensor_); + LOG_SENSOR(" ", "StillTargetEnergy", this->still_target_energy_sensor_); for (sensor::Sensor *s : this->gate_move_sensors_) { - LOG_SENSOR(" ", "NthGateMoveSesnsor", s); + LOG_SENSOR(" ", "GateMove", s); + } + for (sensor::Sensor *s : this->gate_still_sensors_) { + LOG_SENSOR(" ", "GateStill", s); } #endif #ifdef USE_TEXT_SENSOR - LOG_TEXT_SENSOR(" ", "VersionTextSensor", this->version_text_sensor_); - LOG_TEXT_SENSOR(" ", "MacTextSensor", this->mac_text_sensor_); -#endif -#ifdef USE_SELECT - LOG_SELECT(" ", "LightFunctionSelect", this->light_function_select_); - LOG_SELECT(" ", "OutPinLevelSelect", this->out_pin_level_select_); - LOG_SELECT(" ", "DistanceResolutionSelect", this->distance_resolution_select_); - LOG_SELECT(" ", "BaudRateSelect", this->baud_rate_select_); + ESP_LOGCONFIG(TAG, "Text Sensors:"); + LOG_TEXT_SENSOR(" ", "Mac", this->mac_text_sensor_); + LOG_TEXT_SENSOR(" ", "Version", this->version_text_sensor_); #endif #ifdef USE_NUMBER - LOG_NUMBER(" ", "LightThresholdNumber", this->light_threshold_number_); - LOG_NUMBER(" ", "MaxStillDistanceGateNumber", this->max_still_distance_gate_number_); - LOG_NUMBER(" ", "MaxMoveDistanceGateNumber", this->max_move_distance_gate_number_); - LOG_NUMBER(" ", "TimeoutNumber", this->timeout_number_); - for (number::Number *n : this->gate_still_threshold_numbers_) { - LOG_NUMBER(" ", "Still Thresholds Number", n); - } + ESP_LOGCONFIG(TAG, "Numbers:"); + LOG_NUMBER(" ", "LightThreshold", this->light_threshold_number_); + LOG_NUMBER(" ", "MaxMoveDistanceGate", this->max_move_distance_gate_number_); + LOG_NUMBER(" ", "MaxStillDistanceGate", this->max_still_distance_gate_number_); + LOG_NUMBER(" ", "Timeout", this->timeout_number_); for (number::Number *n : this->gate_move_threshold_numbers_) { - LOG_NUMBER(" ", "Move Thresholds Number", n); + LOG_NUMBER(" ", "MoveThreshold", n); + } + for (number::Number *n : this->gate_still_threshold_numbers_) { + LOG_NUMBER(" ", "StillThreshold", n); } #endif - this->read_all_info(); - ESP_LOGCONFIG(TAG, - " Throttle_ : %ums\n" - " MAC Address : %s\n" - " Firmware Version : %s", - this->throttle_, const_cast(this->mac_.c_str()), const_cast(this->version_.c_str())); +#ifdef USE_SELECT + ESP_LOGCONFIG(TAG, "Selects:"); + LOG_SELECT(" ", "BaudRate", this->baud_rate_select_); + LOG_SELECT(" ", "DistanceResolution", this->distance_resolution_select_); + LOG_SELECT(" ", "LightFunction", this->light_function_select_); + LOG_SELECT(" ", "OutPinLevel", this->out_pin_level_select_); +#endif +#ifdef USE_SWITCH + ESP_LOGCONFIG(TAG, "Switches:"); + LOG_SWITCH(" ", "Bluetooth", this->bluetooth_switch_); + LOG_SWITCH(" ", "EngineeringMode", this->engineering_mode_switch_); +#endif +#ifdef USE_BUTTON + ESP_LOGCONFIG(TAG, "Buttons:"); + LOG_BUTTON(" ", "FactoryReset", this->factory_reset_button_); + LOG_BUTTON(" ", "Query", this->query_button_); + LOG_BUTTON(" ", "Restart", this->restart_button_); +#endif } void LD2410Component::setup() { @@ -89,12 +268,12 @@ void LD2410Component::read_all_info() { this->get_version_(); this->get_mac_(); this->get_distance_resolution_(); - this->get_light_control_(); + this->query_light_control_(); this->query_parameters_(); this->set_config_mode_(false); #ifdef USE_SELECT const auto baud_rate = std::to_string(this->parent_->get_baud_rate()); - if (this->baud_rate_select_ != nullptr && this->baud_rate_select_->state != baud_rate) { + if (this->baud_rate_select_ != nullptr) { this->baud_rate_select_->publish_state(baud_rate); } #endif @@ -107,66 +286,59 @@ void LD2410Component::restart_and_read_all_info() { } void LD2410Component::loop() { - const int max_line_length = 80; - static uint8_t buffer[max_line_length]; - - while (available()) { - this->readline_(read(), buffer, max_line_length); + while (this->available()) { + this->readline_(this->read()); } } -void LD2410Component::send_command_(uint8_t command, const uint8_t *command_value, int command_value_len) { +void LD2410Component::send_command_(uint8_t command, const uint8_t *command_value, uint8_t command_value_len) { ESP_LOGV(TAG, "Sending COMMAND %02X", command); - // frame start bytes - this->write_array(CMD_FRAME_HEADER, 4); + // frame header bytes + this->write_array(CMD_FRAME_HEADER, sizeof(CMD_FRAME_HEADER)); // length bytes - int len = 2; - if (command_value != nullptr) + uint8_t len = 2; + if (command_value != nullptr) { len += command_value_len; - this->write_byte(lowbyte(len)); - this->write_byte(highbyte(len)); - - // command - this->write_byte(lowbyte(command)); - this->write_byte(highbyte(command)); + } + uint8_t len_cmd[] = {lowbyte(len), highbyte(len), command, 0x00}; + this->write_array(len_cmd, sizeof(len_cmd)); // command value bytes if (command_value != nullptr) { - for (int i = 0; i < command_value_len; i++) { + for (uint8_t i = 0; i < command_value_len; i++) { this->write_byte(command_value[i]); } } - // frame end bytes - this->write_array(CMD_FRAME_END, 4); + // frame footer bytes + this->write_array(CMD_FRAME_FOOTER, sizeof(CMD_FRAME_FOOTER)); // FIXME to remove delay(50); // NOLINT } -void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) { - if (len < 12) - return; // 4 frame start bytes + 2 length bytes + 1 data end byte + 1 crc byte + 4 frame end bytes - if (buffer[0] != 0xF4 || buffer[1] != 0xF3 || buffer[2] != 0xF2 || buffer[3] != 0xF1) // check 4 frame start bytes +void LD2410Component::handle_periodic_data_() { + // Reduce data update rate to reduce home assistant database growth + // Check this first to prevent unnecessary processing done in later checks/parsing + if (App.get_loop_component_start_time() - this->last_periodic_millis_ < this->throttle_) { return; - if (buffer[7] != HEAD || buffer[len - 6] != END || buffer[len - 5] != CHECK) // Check constant values - return; // data head=0xAA, data end=0x55, crc=0x00 - - /* - Reduce data update rate to prevent home assistant database size grow fast - */ - int32_t current_millis = millis(); - if (current_millis - last_periodic_millis_ < this->throttle_) + } + // 4 frame header bytes + 2 length bytes + 1 data end byte + 1 crc byte + 4 frame footer bytes + // data header=0xAA, data footer=0x55, crc=0x00 + if (this->buffer_pos_ < 12 || !ld2410::validate_header_footer(DATA_FRAME_HEADER, this->buffer_data_) || + this->buffer_data_[7] != HEADER || this->buffer_data_[this->buffer_pos_ - 6] != FOOTER || + this->buffer_data_[this->buffer_pos_ - 5] != CHECK) { return; - last_periodic_millis_ = current_millis; + } + // Save the timestamp after validating the frame so, if invalid, we'll take the next frame immediately + this->last_periodic_millis_ = App.get_loop_component_start_time(); /* Data Type: 7th 0x01: Engineering mode 0x02: Normal mode */ - bool engineering_mode = buffer[DATA_TYPES] == 0x01; + bool engineering_mode = this->buffer_data_[DATA_TYPES] == 0x01; #ifdef USE_SWITCH - if (this->engineering_mode_switch_ != nullptr && - current_millis - last_engineering_mode_change_millis_ > this->throttle_) { + if (this->engineering_mode_switch_ != nullptr) { this->engineering_mode_switch_->publish_state(engineering_mode); } #endif @@ -178,7 +350,7 @@ void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) { 0x02 = Still targets 0x03 = Moving+Still targets */ - char target_state = buffer[TARGET_STATES]; + char target_state = this->buffer_data_[TARGET_STATES]; if (this->target_binary_sensor_ != nullptr) { this->target_binary_sensor_->publish_state(target_state != 0x00); } @@ -198,27 +370,30 @@ void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) { */ #ifdef USE_SENSOR if (this->moving_target_distance_sensor_ != nullptr) { - int new_moving_target_distance = this->two_byte_to_int_(buffer[MOVING_TARGET_LOW], buffer[MOVING_TARGET_HIGH]); + int new_moving_target_distance = + ld2410::two_byte_to_int(this->buffer_data_[MOVING_TARGET_LOW], this->buffer_data_[MOVING_TARGET_HIGH]); if (this->moving_target_distance_sensor_->get_state() != new_moving_target_distance) this->moving_target_distance_sensor_->publish_state(new_moving_target_distance); } if (this->moving_target_energy_sensor_ != nullptr) { - int new_moving_target_energy = buffer[MOVING_ENERGY]; + int new_moving_target_energy = this->buffer_data_[MOVING_ENERGY]; if (this->moving_target_energy_sensor_->get_state() != new_moving_target_energy) this->moving_target_energy_sensor_->publish_state(new_moving_target_energy); } if (this->still_target_distance_sensor_ != nullptr) { - int new_still_target_distance = this->two_byte_to_int_(buffer[STILL_TARGET_LOW], buffer[STILL_TARGET_HIGH]); + int new_still_target_distance = + ld2410::two_byte_to_int(this->buffer_data_[STILL_TARGET_LOW], this->buffer_data_[STILL_TARGET_HIGH]); if (this->still_target_distance_sensor_->get_state() != new_still_target_distance) this->still_target_distance_sensor_->publish_state(new_still_target_distance); } if (this->still_target_energy_sensor_ != nullptr) { - int new_still_target_energy = buffer[STILL_ENERGY]; + int new_still_target_energy = this->buffer_data_[STILL_ENERGY]; if (this->still_target_energy_sensor_->get_state() != new_still_target_energy) this->still_target_energy_sensor_->publish_state(new_still_target_energy); } if (this->detection_distance_sensor_ != nullptr) { - int new_detect_distance = this->two_byte_to_int_(buffer[DETECT_DISTANCE_LOW], buffer[DETECT_DISTANCE_HIGH]); + int new_detect_distance = + ld2410::two_byte_to_int(this->buffer_data_[DETECT_DISTANCE_LOW], this->buffer_data_[DETECT_DISTANCE_HIGH]); if (this->detection_distance_sensor_->get_state() != new_detect_distance) this->detection_distance_sensor_->publish_state(new_detect_distance); } @@ -231,7 +406,7 @@ void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) { for (std::vector::size_type i = 0; i != this->gate_move_sensors_.size(); i++) { sensor::Sensor *s = this->gate_move_sensors_[i]; if (s != nullptr) { - s->publish_state(buffer[MOVING_SENSOR_START + i]); + s->publish_state(this->buffer_data_[MOVING_SENSOR_START + i]); } } /* @@ -240,16 +415,17 @@ void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) { for (std::vector::size_type i = 0; i != this->gate_still_sensors_.size(); i++) { sensor::Sensor *s = this->gate_still_sensors_[i]; if (s != nullptr) { - s->publish_state(buffer[STILL_SENSOR_START + i]); + s->publish_state(this->buffer_data_[STILL_SENSOR_START + i]); } } /* Light sensor: 38th bytes */ if (this->light_sensor_ != nullptr) { - int new_light_sensor = buffer[LIGHT_SENSOR]; - if (this->light_sensor_->get_state() != new_light_sensor) + int new_light_sensor = this->buffer_data_[LIGHT_SENSOR]; + if (this->light_sensor_->get_state() != new_light_sensor) { this->light_sensor_->publish_state(new_light_sensor); + } } } else { for (auto *s : this->gate_move_sensors_) { @@ -270,7 +446,7 @@ void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) { #ifdef USE_BINARY_SENSOR if (engineering_mode) { if (this->out_pin_presence_status_binary_sensor_ != nullptr) { - this->out_pin_presence_status_binary_sensor_->publish_state(buffer[OUT_PIN_SENSOR] == 0x01); + this->out_pin_presence_status_binary_sensor_->publish_state(this->buffer_data_[OUT_PIN_SENSOR] == 0x01); } } else { if (this->out_pin_presence_status_binary_sensor_ != nullptr) { @@ -280,163 +456,151 @@ void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) { #endif } -const char VERSION_FMT[] = "%u.%02X.%02X%02X%02X%02X"; - -std::string format_version(uint8_t *buffer) { - std::string::size_type version_size = 256; - std::string version; - do { - version.resize(version_size + 1); - version_size = std::snprintf(&version[0], version.size(), VERSION_FMT, buffer[13], buffer[12], buffer[17], - buffer[16], buffer[15], buffer[14]); - } while (version_size + 1 > version.size()); - version.resize(version_size); - return version; -} - -const char MAC_FMT[] = "%02X:%02X:%02X:%02X:%02X:%02X"; - -const std::string UNKNOWN_MAC("unknown"); -const std::string NO_MAC("08:05:04:03:02:01"); - -std::string format_mac(uint8_t *buffer) { - std::string::size_type mac_size = 256; - std::string mac; - do { - mac.resize(mac_size + 1); - mac_size = std::snprintf(&mac[0], mac.size(), MAC_FMT, buffer[10], buffer[11], buffer[12], buffer[13], buffer[14], - buffer[15]); - } while (mac_size + 1 > mac.size()); - mac.resize(mac_size); - if (mac == NO_MAC) { - return UNKNOWN_MAC; - } - return mac; -} - #ifdef USE_NUMBER std::function set_number_value(number::Number *n, float value) { - float normalized_value = value * 1.0; - if (n != nullptr && (!n->has_state() || n->state != normalized_value)) { - n->state = normalized_value; - return [n, normalized_value]() { n->publish_state(normalized_value); }; + if (n != nullptr && (!n->has_state() || n->state != value)) { + n->state = value; + return [n, value]() { n->publish_state(value); }; } return []() {}; } #endif -bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) { - ESP_LOGV(TAG, "Handling ACK DATA for COMMAND %02X", buffer[COMMAND]); - if (len < 10) { - ESP_LOGE(TAG, "Error with last command : incorrect length"); +bool LD2410Component::handle_ack_data_() { + ESP_LOGV(TAG, "Handling ACK DATA for COMMAND %02X", this->buffer_data_[COMMAND]); + if (this->buffer_pos_ < 10) { + ESP_LOGE(TAG, "Invalid length"); return true; } - if (buffer[0] != 0xFD || buffer[1] != 0xFC || buffer[2] != 0xFB || buffer[3] != 0xFA) { // check 4 frame start bytes - ESP_LOGE(TAG, "Error with last command : incorrect Header"); + if (!ld2410::validate_header_footer(CMD_FRAME_HEADER, this->buffer_data_)) { + ESP_LOGW(TAG, "Invalid header: %s", format_hex_pretty(this->buffer_data_, HEADER_FOOTER_SIZE).c_str()); return true; } - if (buffer[COMMAND_STATUS] != 0x01) { - ESP_LOGE(TAG, "Error with last command : status != 0x01"); + if (this->buffer_data_[COMMAND_STATUS] != 0x01) { + ESP_LOGE(TAG, "Invalid status"); return true; } - if (this->two_byte_to_int_(buffer[8], buffer[9]) != 0x00) { - ESP_LOGE(TAG, "Error with last command , last buffer was: %u , %u", buffer[8], buffer[9]); + if (ld2410::two_byte_to_int(this->buffer_data_[8], this->buffer_data_[9]) != 0x00) { + ESP_LOGW(TAG, "Invalid command: %02X, %02X", this->buffer_data_[8], this->buffer_data_[9]); return true; } - switch (buffer[COMMAND]) { - case lowbyte(CMD_ENABLE_CONF): - ESP_LOGV(TAG, "Handled Enable conf command"); + switch (this->buffer_data_[COMMAND]) { + case CMD_ENABLE_CONF: + ESP_LOGV(TAG, "Enable conf"); break; - case lowbyte(CMD_DISABLE_CONF): - ESP_LOGV(TAG, "Handled Disabled conf command"); + + case CMD_DISABLE_CONF: + ESP_LOGV(TAG, "Disabled conf"); break; - case lowbyte(CMD_SET_BAUD_RATE): - ESP_LOGV(TAG, "Handled baud rate change command"); + + case CMD_SET_BAUD_RATE: + ESP_LOGV(TAG, "Baud rate change"); #ifdef USE_SELECT if (this->baud_rate_select_ != nullptr) { - ESP_LOGE(TAG, "Change baud rate component config to %s and reinstall", this->baud_rate_select_->state.c_str()); + ESP_LOGE(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->state.c_str()); } #endif break; - case lowbyte(CMD_VERSION): - this->version_ = format_version(buffer); - ESP_LOGV(TAG, "FW Version is: %s", const_cast(this->version_.c_str())); + + case CMD_QUERY_VERSION: { + std::memcpy(this->version_, &this->buffer_data_[12], sizeof(this->version_)); + std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5], + this->version_[4], this->version_[3], this->version_[2]); + ESP_LOGV(TAG, "Firmware version: %s", version.c_str()); #ifdef USE_TEXT_SENSOR if (this->version_text_sensor_ != nullptr) { - this->version_text_sensor_->publish_state(this->version_); + this->version_text_sensor_->publish_state(version); } #endif break; - case lowbyte(CMD_QUERY_DISTANCE_RESOLUTION): { - std::string distance_resolution = - DISTANCE_RESOLUTION_INT_TO_ENUM.at(this->two_byte_to_int_(buffer[10], buffer[11])); - ESP_LOGV(TAG, "Distance resolution is: %s", const_cast(distance_resolution.c_str())); + } + + case CMD_QUERY_DISTANCE_RESOLUTION: { + const auto *distance_resolution = find_str(DISTANCE_RESOLUTIONS_BY_UINT, this->buffer_data_[10]); + ESP_LOGV(TAG, "Distance resolution: %s", distance_resolution); #ifdef USE_SELECT - if (this->distance_resolution_select_ != nullptr && - this->distance_resolution_select_->state != distance_resolution) { + if (this->distance_resolution_select_ != nullptr) { this->distance_resolution_select_->publish_state(distance_resolution); } #endif - } break; - case lowbyte(CMD_QUERY_LIGHT_CONTROL): { - this->light_function_ = LIGHT_FUNCTION_INT_TO_ENUM.at(buffer[10]); - this->light_threshold_ = buffer[11] * 1.0; - this->out_pin_level_ = OUT_PIN_LEVEL_INT_TO_ENUM.at(buffer[12]); - ESP_LOGV(TAG, "Light function is: %s", const_cast(this->light_function_.c_str())); - ESP_LOGV(TAG, "Light threshold is: %f", this->light_threshold_); - ESP_LOGV(TAG, "Out pin level is: %s", const_cast(this->out_pin_level_.c_str())); + break; + } + + case CMD_QUERY_LIGHT_CONTROL: { + this->light_function_ = this->buffer_data_[10]; + this->light_threshold_ = this->buffer_data_[11]; + this->out_pin_level_ = this->buffer_data_[12]; + 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_); + ESP_LOGV(TAG, + "Light function is: %s\n" + "Light threshold is: %u\n" + "Out pin level: %s", + light_function_str, this->light_threshold_, out_pin_level_str); #ifdef USE_SELECT - if (this->light_function_select_ != nullptr && this->light_function_select_->state != this->light_function_) { - this->light_function_select_->publish_state(this->light_function_); + if (this->light_function_select_ != nullptr) { + this->light_function_select_->publish_state(light_function_str); } - if (this->out_pin_level_select_ != nullptr && this->out_pin_level_select_->state != this->out_pin_level_) { - this->out_pin_level_select_->publish_state(this->out_pin_level_); + if (this->out_pin_level_select_ != nullptr) { + this->out_pin_level_select_->publish_state(out_pin_level_str); } #endif #ifdef USE_NUMBER - if (this->light_threshold_number_ != nullptr && - (!this->light_threshold_number_->has_state() || - this->light_threshold_number_->state != this->light_threshold_)) { - this->light_threshold_number_->publish_state(this->light_threshold_); + if (this->light_threshold_number_ != nullptr) { + this->light_threshold_number_->publish_state(static_cast(this->light_threshold_)); } #endif - } break; - case lowbyte(CMD_MAC): - if (len < 20) { + break; + } + case CMD_QUERY_MAC_ADDRESS: { + if (this->buffer_pos_ < 20) { return false; } - this->mac_ = format_mac(buffer); - ESP_LOGV(TAG, "MAC Address is: %s", const_cast(this->mac_.c_str())); + + this->bluetooth_on_ = std::memcmp(&this->buffer_data_[10], NO_MAC, sizeof(NO_MAC)) != 0; + if (this->bluetooth_on_) { + std::memcpy(this->mac_address_, &this->buffer_data_[10], sizeof(this->mac_address_)); + } + + std::string mac_str = + mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC; + ESP_LOGV(TAG, "MAC address: %s", mac_str.c_str()); #ifdef USE_TEXT_SENSOR if (this->mac_text_sensor_ != nullptr) { - this->mac_text_sensor_->publish_state(this->mac_); + this->mac_text_sensor_->publish_state(mac_str); } #endif #ifdef USE_SWITCH if (this->bluetooth_switch_ != nullptr) { - this->bluetooth_switch_->publish_state(this->mac_ != UNKNOWN_MAC); + this->bluetooth_switch_->publish_state(this->bluetooth_on_); } #endif break; - case lowbyte(CMD_GATE_SENS): - ESP_LOGV(TAG, "Handled sensitivity command"); + } + + case CMD_GATE_SENS: + ESP_LOGV(TAG, "Sensitivity"); break; - case lowbyte(CMD_BLUETOOTH): - ESP_LOGV(TAG, "Handled bluetooth command"); + + case CMD_BLUETOOTH: + ESP_LOGV(TAG, "Bluetooth"); break; - case lowbyte(CMD_SET_DISTANCE_RESOLUTION): - ESP_LOGV(TAG, "Handled set distance resolution command"); + + case CMD_SET_DISTANCE_RESOLUTION: + ESP_LOGV(TAG, "Set distance resolution"); break; - case lowbyte(CMD_SET_LIGHT_CONTROL): - ESP_LOGV(TAG, "Handled set light control command"); + + case CMD_SET_LIGHT_CONTROL: + ESP_LOGV(TAG, "Set light control"); break; - case lowbyte(CMD_BT_PASSWORD): - ESP_LOGV(TAG, "Handled set bluetooth password command"); + + case CMD_BT_PASSWORD: + ESP_LOGV(TAG, "Set bluetooth password"); break; - case lowbyte(CMD_QUERY): // Query parameters response - { - if (buffer[10] != 0xAA) + + case CMD_QUERY: { // Query parameters response + if (this->buffer_data_[10] != 0xAA) return true; // value head=0xAA #ifdef USE_NUMBER /* @@ -444,29 +608,31 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) { Still distance range: 14th byte */ std::vector> updates; - updates.push_back(set_number_value(this->max_move_distance_gate_number_, buffer[12])); - updates.push_back(set_number_value(this->max_still_distance_gate_number_, buffer[13])); + updates.push_back(set_number_value(this->max_move_distance_gate_number_, this->buffer_data_[12])); + updates.push_back(set_number_value(this->max_still_distance_gate_number_, this->buffer_data_[13])); /* Moving Sensitivities: 15~23th bytes */ for (std::vector::size_type i = 0; i != this->gate_move_threshold_numbers_.size(); i++) { - updates.push_back(set_number_value(this->gate_move_threshold_numbers_[i], buffer[14 + i])); + updates.push_back(set_number_value(this->gate_move_threshold_numbers_[i], this->buffer_data_[14 + i])); } /* Still Sensitivities: 24~32th bytes */ for (std::vector::size_type i = 0; i != this->gate_still_threshold_numbers_.size(); i++) { - updates.push_back(set_number_value(this->gate_still_threshold_numbers_[i], buffer[23 + i])); + updates.push_back(set_number_value(this->gate_still_threshold_numbers_[i], this->buffer_data_[23 + i])); } /* None Duration: 33~34th bytes */ - updates.push_back(set_number_value(this->timeout_number_, this->two_byte_to_int_(buffer[32], buffer[33]))); + updates.push_back(set_number_value(this->timeout_number_, + ld2410::two_byte_to_int(this->buffer_data_[32], this->buffer_data_[33]))); for (auto &update : updates) { update(); } #endif - } break; + break; + } default: break; } @@ -474,78 +640,84 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) { return true; } -void LD2410Component::readline_(int readch, uint8_t *buffer, int len) { - static int pos = 0; +void LD2410Component::readline_(int readch) { + if (readch < 0) { + return; // No data available + } - if (readch >= 0) { - if (pos < len - 1) { - buffer[pos++] = readch; - buffer[pos] = 0; + if (this->buffer_pos_ < MAX_LINE_LENGTH - 1) { + this->buffer_data_[this->buffer_pos_++] = readch; + this->buffer_data_[this->buffer_pos_] = 0; + } else { + // We should never get here, but just in case... + ESP_LOGW(TAG, "Max command length exceeded; ignoring"); + this->buffer_pos_ = 0; + } + if (this->buffer_pos_ < 4) { + return; // Not enough data to process yet + } + if (this->buffer_data_[this->buffer_pos_ - 4] == DATA_FRAME_FOOTER[0] && + 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()); + this->handle_periodic_data_(); + this->buffer_pos_ = 0; // Reset position index for next message + } else if (this->buffer_data_[this->buffer_pos_ - 4] == CMD_FRAME_FOOTER[0] && + 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()); + if (this->handle_ack_data_()) { + this->buffer_pos_ = 0; // Reset position index for next message } else { - pos = 0; - } - if (pos >= 4) { - if (buffer[pos - 4] == 0xF8 && buffer[pos - 3] == 0xF7 && buffer[pos - 2] == 0xF6 && buffer[pos - 1] == 0xF5) { - ESP_LOGV(TAG, "Will handle Periodic Data"); - this->handle_periodic_data_(buffer, pos); - pos = 0; // Reset position index ready for next time - } else if (buffer[pos - 4] == 0x04 && buffer[pos - 3] == 0x03 && buffer[pos - 2] == 0x02 && - buffer[pos - 1] == 0x01) { - ESP_LOGV(TAG, "Will handle ACK Data"); - if (this->handle_ack_data_(buffer, pos)) { - pos = 0; // Reset position index ready for next time - } else { - ESP_LOGV(TAG, "ACK Data incomplete"); - } - } + ESP_LOGV(TAG, "Ack Data incomplete"); } } } void LD2410Component::set_config_mode_(bool enable) { - uint8_t cmd = enable ? CMD_ENABLE_CONF : CMD_DISABLE_CONF; - uint8_t cmd_value[2] = {0x01, 0x00}; - this->send_command_(cmd, enable ? cmd_value : nullptr, 2); + const uint8_t cmd = enable ? CMD_ENABLE_CONF : CMD_DISABLE_CONF; + const uint8_t cmd_value[2] = {0x01, 0x00}; + this->send_command_(cmd, enable ? cmd_value : nullptr, sizeof(cmd_value)); } void LD2410Component::set_bluetooth(bool enable) { this->set_config_mode_(true); - uint8_t enable_cmd_value[2] = {0x01, 0x00}; - uint8_t disable_cmd_value[2] = {0x00, 0x00}; - this->send_command_(CMD_BLUETOOTH, enable ? enable_cmd_value : disable_cmd_value, 2); + const uint8_t cmd_value[2] = {enable ? (uint8_t) 0x01 : (uint8_t) 0x00, 0x00}; + this->send_command_(CMD_BLUETOOTH, cmd_value, sizeof(cmd_value)); this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); } void LD2410Component::set_distance_resolution(const std::string &state) { this->set_config_mode_(true); - uint8_t cmd_value[2] = {DISTANCE_RESOLUTION_ENUM_TO_INT.at(state), 0x00}; - this->send_command_(CMD_SET_DISTANCE_RESOLUTION, cmd_value, 2); + const uint8_t cmd_value[2] = {find_uint8(DISTANCE_RESOLUTIONS_BY_STR, state), 0x00}; + this->send_command_(CMD_SET_DISTANCE_RESOLUTION, cmd_value, sizeof(cmd_value)); this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); } void LD2410Component::set_baud_rate(const std::string &state) { this->set_config_mode_(true); - uint8_t cmd_value[2] = {BAUD_RATE_ENUM_TO_INT.at(state), 0x00}; - this->send_command_(CMD_SET_BAUD_RATE, cmd_value, 2); + const uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00}; + this->send_command_(CMD_SET_BAUD_RATE, cmd_value, sizeof(cmd_value)); this->set_timeout(200, [this]() { this->restart_(); }); } void LD2410Component::set_bluetooth_password(const std::string &password) { if (password.length() != 6) { - ESP_LOGE(TAG, "set_bluetooth_password(): invalid password length, must be exactly 6 chars '%s'", password.c_str()); + ESP_LOGE(TAG, "Password must be exactly 6 chars"); return; } this->set_config_mode_(true); uint8_t cmd_value[6]; std::copy(password.begin(), password.end(), std::begin(cmd_value)); - this->send_command_(CMD_BT_PASSWORD, cmd_value, 6); + this->send_command_(CMD_BT_PASSWORD, cmd_value, sizeof(cmd_value)); this->set_config_mode_(false); } void LD2410Component::set_engineering_mode(bool enable) { + const uint8_t cmd = enable ? CMD_ENABLE_ENG : CMD_DISABLE_ENG; this->set_config_mode_(true); - last_engineering_mode_change_millis_ = millis(); - uint8_t cmd = enable ? CMD_ENABLE_ENG : CMD_DISABLE_ENG; this->send_command_(cmd, nullptr, 0); this->set_config_mode_(false); } @@ -559,14 +731,17 @@ void LD2410Component::factory_reset() { void LD2410Component::restart_() { this->send_command_(CMD_RESTART, nullptr, 0); } void LD2410Component::query_parameters_() { this->send_command_(CMD_QUERY, nullptr, 0); } -void LD2410Component::get_version_() { this->send_command_(CMD_VERSION, nullptr, 0); } + +void LD2410Component::get_version_() { this->send_command_(CMD_QUERY_VERSION, nullptr, 0); } + void LD2410Component::get_mac_() { - uint8_t cmd_value[2] = {0x01, 0x00}; - this->send_command_(CMD_MAC, cmd_value, 2); + const uint8_t cmd_value[2] = {0x01, 0x00}; + this->send_command_(CMD_QUERY_MAC_ADDRESS, cmd_value, sizeof(cmd_value)); } + void LD2410Component::get_distance_resolution_() { this->send_command_(CMD_QUERY_DISTANCE_RESOLUTION, nullptr, 0); } -void LD2410Component::get_light_control_() { this->send_command_(CMD_QUERY_LIGHT_CONTROL, nullptr, 0); } +void LD2410Component::query_light_control_() { this->send_command_(CMD_QUERY_LIGHT_CONTROL, nullptr, 0); } #ifdef USE_NUMBER void LD2410Component::set_max_distances_timeout() { @@ -596,7 +771,7 @@ void LD2410Component::set_max_distances_timeout() { 0x00, 0x00}; this->set_config_mode_(true); - this->send_command_(CMD_MAXDIST_DURATION, value, 18); + this->send_command_(CMD_MAXDIST_DURATION, value, sizeof(value)); delay(50); // NOLINT this->query_parameters_(); this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); @@ -626,17 +801,17 @@ void LD2410Component::set_gate_threshold(uint8_t gate) { uint8_t value[18] = {0x00, 0x00, lowbyte(gate), highbyte(gate), 0x00, 0x00, 0x01, 0x00, lowbyte(motion), highbyte(motion), 0x00, 0x00, 0x02, 0x00, lowbyte(still), highbyte(still), 0x00, 0x00}; - this->send_command_(CMD_GATE_SENS, value, 18); + this->send_command_(CMD_GATE_SENS, value, sizeof(value)); delay(50); // NOLINT this->query_parameters_(); this->set_config_mode_(false); } -void LD2410Component::set_gate_still_threshold_number(int gate, number::Number *n) { +void LD2410Component::set_gate_still_threshold_number(uint8_t gate, number::Number *n) { this->gate_still_threshold_numbers_[gate] = n; } -void LD2410Component::set_gate_move_threshold_number(int gate, number::Number *n) { +void LD2410Component::set_gate_move_threshold_number(uint8_t gate, number::Number *n) { this->gate_move_threshold_numbers_[gate] = n; } #endif @@ -644,35 +819,29 @@ void LD2410Component::set_gate_move_threshold_number(int gate, number::Number *n void LD2410Component::set_light_out_control() { #ifdef USE_NUMBER if (this->light_threshold_number_ != nullptr && this->light_threshold_number_->has_state()) { - this->light_threshold_ = this->light_threshold_number_->state; + this->light_threshold_ = static_cast(this->light_threshold_number_->state); } #endif #ifdef USE_SELECT if (this->light_function_select_ != nullptr && this->light_function_select_->has_state()) { - this->light_function_ = this->light_function_select_->state; + this->light_function_ = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_select_->state); } if (this->out_pin_level_select_ != nullptr && this->out_pin_level_select_->has_state()) { - this->out_pin_level_ = this->out_pin_level_select_->state; + this->out_pin_level_ = find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->state); } #endif - if (this->light_function_.empty() || this->out_pin_level_.empty() || this->light_threshold_ < 0) { - return; - } this->set_config_mode_(true); - uint8_t light_function = LIGHT_FUNCTION_ENUM_TO_INT.at(this->light_function_); - uint8_t light_threshold = static_cast(this->light_threshold_); - uint8_t out_pin_level = OUT_PIN_LEVEL_ENUM_TO_INT.at(this->out_pin_level_); - uint8_t value[4] = {light_function, light_threshold, out_pin_level, 0x00}; - this->send_command_(CMD_SET_LIGHT_CONTROL, value, 4); + 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)); delay(50); // NOLINT - this->get_light_control_(); + this->query_light_control_(); this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); this->set_config_mode_(false); } #ifdef USE_SENSOR -void LD2410Component::set_gate_move_sensor(int gate, sensor::Sensor *s) { this->gate_move_sensors_[gate] = s; } -void LD2410Component::set_gate_still_sensor(int gate, sensor::Sensor *s) { this->gate_still_sensors_[gate] = s; } +void LD2410Component::set_gate_move_sensor(uint8_t gate, sensor::Sensor *s) { this->gate_move_sensors_[gate] = s; } +void LD2410Component::set_gate_still_sensor(uint8_t gate, sensor::Sensor *s) { this->gate_still_sensors_[gate] = s; } #endif } // namespace ld2410 diff --git a/esphome/components/ld2410/ld2410.h b/esphome/components/ld2410/ld2410.h index 1bbaa8987a..8bd1dbcb5a 100644 --- a/esphome/components/ld2410/ld2410.h +++ b/esphome/components/ld2410/ld2410.h @@ -26,170 +26,67 @@ #include "esphome/core/automation.h" #include "esphome/core/helpers.h" -#include - namespace esphome { namespace ld2410 { -#define CHECK_BIT(var, pos) (((var) >> (pos)) & 1) +static const uint8_t MAX_LINE_LENGTH = 46; // Max characters for serial buffer +static const uint8_t TOTAL_GATES = 9; // Total number of gates supported by the LD2410 -// Commands -static const uint8_t CMD_ENABLE_CONF = 0x00FF; -static const uint8_t CMD_DISABLE_CONF = 0x00FE; -static const uint8_t CMD_ENABLE_ENG = 0x0062; -static const uint8_t CMD_DISABLE_ENG = 0x0063; -static const uint8_t CMD_MAXDIST_DURATION = 0x0060; -static const uint8_t CMD_QUERY = 0x0061; -static const uint8_t CMD_GATE_SENS = 0x0064; -static const uint8_t CMD_VERSION = 0x00A0; -static const uint8_t CMD_QUERY_DISTANCE_RESOLUTION = 0x00AB; -static const uint8_t CMD_SET_DISTANCE_RESOLUTION = 0x00AA; -static const uint8_t CMD_QUERY_LIGHT_CONTROL = 0x00AE; -static const uint8_t CMD_SET_LIGHT_CONTROL = 0x00AD; -static const uint8_t CMD_SET_BAUD_RATE = 0x00A1; -static const uint8_t CMD_BT_PASSWORD = 0x00A9; -static const uint8_t CMD_MAC = 0x00A5; -static const uint8_t CMD_RESET = 0x00A2; -static const uint8_t CMD_RESTART = 0x00A3; -static const uint8_t CMD_BLUETOOTH = 0x00A4; - -enum BaudRateStructure : uint8_t { - BAUD_RATE_9600 = 1, - BAUD_RATE_19200 = 2, - BAUD_RATE_38400 = 3, - BAUD_RATE_57600 = 4, - BAUD_RATE_115200 = 5, - BAUD_RATE_230400 = 6, - BAUD_RATE_256000 = 7, - BAUD_RATE_460800 = 8 -}; - -static const std::map BAUD_RATE_ENUM_TO_INT{ - {"9600", BAUD_RATE_9600}, {"19200", BAUD_RATE_19200}, {"38400", BAUD_RATE_38400}, - {"57600", BAUD_RATE_57600}, {"115200", BAUD_RATE_115200}, {"230400", BAUD_RATE_230400}, - {"256000", BAUD_RATE_256000}, {"460800", BAUD_RATE_460800}}; - -enum DistanceResolutionStructure : uint8_t { DISTANCE_RESOLUTION_0_2 = 0x01, DISTANCE_RESOLUTION_0_75 = 0x00 }; - -static const std::map DISTANCE_RESOLUTION_ENUM_TO_INT{{"0.2m", DISTANCE_RESOLUTION_0_2}, - {"0.75m", DISTANCE_RESOLUTION_0_75}}; -static const std::map DISTANCE_RESOLUTION_INT_TO_ENUM{{DISTANCE_RESOLUTION_0_2, "0.2m"}, - {DISTANCE_RESOLUTION_0_75, "0.75m"}}; - -enum LightFunctionStructure : uint8_t { - LIGHT_FUNCTION_OFF = 0x00, - LIGHT_FUNCTION_BELOW = 0x01, - LIGHT_FUNCTION_ABOVE = 0x02 -}; - -static const std::map LIGHT_FUNCTION_ENUM_TO_INT{ - {"off", LIGHT_FUNCTION_OFF}, {"below", LIGHT_FUNCTION_BELOW}, {"above", LIGHT_FUNCTION_ABOVE}}; -static const std::map LIGHT_FUNCTION_INT_TO_ENUM{ - {LIGHT_FUNCTION_OFF, "off"}, {LIGHT_FUNCTION_BELOW, "below"}, {LIGHT_FUNCTION_ABOVE, "above"}}; - -enum OutPinLevelStructure : uint8_t { OUT_PIN_LEVEL_LOW = 0x00, OUT_PIN_LEVEL_HIGH = 0x01 }; - -static const std::map OUT_PIN_LEVEL_ENUM_TO_INT{{"low", OUT_PIN_LEVEL_LOW}, - {"high", OUT_PIN_LEVEL_HIGH}}; -static const std::map OUT_PIN_LEVEL_INT_TO_ENUM{{OUT_PIN_LEVEL_LOW, "low"}, - {OUT_PIN_LEVEL_HIGH, "high"}}; - -// Commands values -static const uint8_t CMD_MAX_MOVE_VALUE = 0x0000; -static const uint8_t CMD_MAX_STILL_VALUE = 0x0001; -static const uint8_t CMD_DURATION_VALUE = 0x0002; -// Command Header & Footer -static const uint8_t CMD_FRAME_HEADER[4] = {0xFD, 0xFC, 0xFB, 0xFA}; -static const uint8_t CMD_FRAME_END[4] = {0x04, 0x03, 0x02, 0x01}; -// Data Header & Footer -static const uint8_t DATA_FRAME_HEADER[4] = {0xF4, 0xF3, 0xF2, 0xF1}; -static const uint8_t DATA_FRAME_END[4] = {0xF8, 0xF7, 0xF6, 0xF5}; -/* -Data Type: 6th byte -Target states: 9th byte - Moving target distance: 10~11th bytes - Moving target energy: 12th byte - Still target distance: 13~14th bytes - Still target energy: 15th byte - Detect distance: 16~17th bytes -*/ -enum PeriodicDataStructure : uint8_t { - DATA_TYPES = 6, - TARGET_STATES = 8, - MOVING_TARGET_LOW = 9, - MOVING_TARGET_HIGH = 10, - MOVING_ENERGY = 11, - STILL_TARGET_LOW = 12, - STILL_TARGET_HIGH = 13, - STILL_ENERGY = 14, - DETECT_DISTANCE_LOW = 15, - DETECT_DISTANCE_HIGH = 16, - MOVING_SENSOR_START = 19, - STILL_SENSOR_START = 28, - LIGHT_SENSOR = 37, - OUT_PIN_SENSOR = 38, -}; -enum PeriodicDataValue : uint8_t { HEAD = 0xAA, END = 0x55, CHECK = 0x00 }; - -enum AckDataStructure : uint8_t { COMMAND = 6, COMMAND_STATUS = 7 }; - -// char cmd[2] = {enable ? 0xFF : 0xFE, 0x00}; class LD2410Component : public Component, public uart::UARTDevice { -#ifdef USE_SENSOR - SUB_SENSOR(moving_target_distance) - SUB_SENSOR(still_target_distance) - SUB_SENSOR(moving_target_energy) - SUB_SENSOR(still_target_energy) - SUB_SENSOR(light) - SUB_SENSOR(detection_distance) -#endif #ifdef USE_BINARY_SENSOR - SUB_BINARY_SENSOR(target) + SUB_BINARY_SENSOR(out_pin_presence_status) SUB_BINARY_SENSOR(moving_target) SUB_BINARY_SENSOR(still_target) - SUB_BINARY_SENSOR(out_pin_presence_status) + SUB_BINARY_SENSOR(target) +#endif +#ifdef USE_SENSOR + SUB_SENSOR(light) + SUB_SENSOR(detection_distance) + SUB_SENSOR(moving_target_distance) + SUB_SENSOR(moving_target_energy) + SUB_SENSOR(still_target_distance) + SUB_SENSOR(still_target_energy) #endif #ifdef USE_TEXT_SENSOR SUB_TEXT_SENSOR(version) SUB_TEXT_SENSOR(mac) #endif +#ifdef USE_NUMBER + SUB_NUMBER(light_threshold) + SUB_NUMBER(max_move_distance_gate) + SUB_NUMBER(max_still_distance_gate) + SUB_NUMBER(timeout) +#endif #ifdef USE_SELECT - SUB_SELECT(distance_resolution) SUB_SELECT(baud_rate) + SUB_SELECT(distance_resolution) SUB_SELECT(light_function) SUB_SELECT(out_pin_level) #endif #ifdef USE_SWITCH - SUB_SWITCH(engineering_mode) SUB_SWITCH(bluetooth) + SUB_SWITCH(engineering_mode) #endif #ifdef USE_BUTTON - SUB_BUTTON(reset) - SUB_BUTTON(restart) + SUB_BUTTON(factory_reset) SUB_BUTTON(query) -#endif -#ifdef USE_NUMBER - SUB_NUMBER(max_still_distance_gate) - SUB_NUMBER(max_move_distance_gate) - SUB_NUMBER(timeout) - SUB_NUMBER(light_threshold) + SUB_BUTTON(restart) #endif public: - LD2410Component(); void setup() override; void dump_config() override; void loop() override; void set_light_out_control(); #ifdef USE_NUMBER - void set_gate_still_threshold_number(int gate, number::Number *n); - void set_gate_move_threshold_number(int gate, number::Number *n); + void set_gate_still_threshold_number(uint8_t gate, number::Number *n); + void set_gate_move_threshold_number(uint8_t gate, number::Number *n); void set_max_distances_timeout(); void set_gate_threshold(uint8_t gate); #endif #ifdef USE_SENSOR - void set_gate_move_sensor(int gate, sensor::Sensor *s); - void set_gate_still_sensor(int gate, sensor::Sensor *s); + void set_gate_move_sensor(uint8_t gate, sensor::Sensor *s); + void set_gate_still_sensor(uint8_t gate, sensor::Sensor *s); #endif void set_throttle(uint16_t value) { this->throttle_ = value; }; void set_bluetooth_password(const std::string &password); @@ -202,34 +99,35 @@ class LD2410Component : public Component, public uart::UARTDevice { void factory_reset(); protected: - int two_byte_to_int_(char firstbyte, char secondbyte) { return (int16_t) (secondbyte << 8) + firstbyte; } - void send_command_(uint8_t command_str, const uint8_t *command_value, int command_value_len); + void send_command_(uint8_t command_str, const uint8_t *command_value, uint8_t command_value_len); void set_config_mode_(bool enable); - void handle_periodic_data_(uint8_t *buffer, int len); - bool handle_ack_data_(uint8_t *buffer, int len); - void readline_(int readch, uint8_t *buffer, int len); + void handle_periodic_data_(); + bool handle_ack_data_(); + void readline_(int readch); void query_parameters_(); void get_version_(); void get_mac_(); void get_distance_resolution_(); - void get_light_control_(); + void query_light_control_(); void restart_(); - int32_t last_periodic_millis_ = millis(); - int32_t last_engineering_mode_change_millis_ = millis(); - uint16_t throttle_; - std::string version_; - std::string mac_; - std::string out_pin_level_; - std::string light_function_; - float light_threshold_ = -1; + uint32_t last_periodic_millis_ = 0; + uint16_t throttle_ = 0; + uint8_t light_function_ = 0; + uint8_t light_threshold_ = 0; + uint8_t out_pin_level_ = 0; + uint8_t buffer_pos_ = 0; // where to resume processing/populating buffer + uint8_t buffer_data_[MAX_LINE_LENGTH]; + uint8_t mac_address_[6] = {0, 0, 0, 0, 0, 0}; + uint8_t version_[6] = {0, 0, 0, 0, 0, 0}; + bool bluetooth_on_{false}; #ifdef USE_NUMBER - std::vector gate_still_threshold_numbers_ = std::vector(9); - std::vector gate_move_threshold_numbers_ = std::vector(9); + std::vector gate_move_threshold_numbers_ = std::vector(TOTAL_GATES); + std::vector gate_still_threshold_numbers_ = std::vector(TOTAL_GATES); #endif #ifdef USE_SENSOR - std::vector gate_still_sensors_ = std::vector(9); - std::vector gate_move_sensors_ = std::vector(9); + std::vector gate_move_sensors_ = std::vector(TOTAL_GATES); + std::vector gate_still_sensors_ = std::vector(TOTAL_GATES); #endif }; diff --git a/esphome/components/ld2410/number/__init__.py b/esphome/components/ld2410/number/__init__.py index 1f9c50db1f..ffa4e7e146 100644 --- a/esphome/components/ld2410/number/__init__.py +++ b/esphome/components/ld2410/number/__init__.py @@ -3,6 +3,8 @@ from esphome.components import number import esphome.config_validation as cv from esphome.const import ( CONF_ID, + CONF_MOVE_THRESHOLD, + CONF_STILL_THRESHOLD, CONF_TIMEOUT, DEVICE_CLASS_DISTANCE, DEVICE_CLASS_ILLUMINANCE, @@ -24,8 +26,6 @@ MaxDistanceTimeoutNumber = ld2410_ns.class_("MaxDistanceTimeoutNumber", number.N CONF_MAX_MOVE_DISTANCE_GATE = "max_move_distance_gate" CONF_MAX_STILL_DISTANCE_GATE = "max_still_distance_gate" CONF_LIGHT_THRESHOLD = "light_threshold" -CONF_STILL_THRESHOLD = "still_threshold" -CONF_MOVE_THRESHOLD = "move_threshold" TIMEOUT_GROUP = "timeout" diff --git a/esphome/components/ld2410/sensor.py b/esphome/components/ld2410/sensor.py index 38de1799cc..92245ea9a6 100644 --- a/esphome/components/ld2410/sensor.py +++ b/esphome/components/ld2410/sensor.py @@ -3,6 +3,7 @@ from esphome.components import sensor import esphome.config_validation as cv from esphome.const import ( CONF_LIGHT, + CONF_MOVING_DISTANCE, DEVICE_CLASS_DISTANCE, DEVICE_CLASS_ILLUMINANCE, ENTITY_CATEGORY_DIAGNOSTIC, @@ -17,7 +18,6 @@ from esphome.const import ( from . import CONF_LD2410_ID, LD2410Component DEPENDENCIES = ["ld2410"] -CONF_MOVING_DISTANCE = "moving_distance" CONF_STILL_DISTANCE = "still_distance" CONF_MOVING_ENERGY = "moving_energy" CONF_STILL_ENERGY = "still_energy" diff --git a/esphome/components/ld2410/switch/__init__.py b/esphome/components/ld2410/switch/__init__.py index aecad606be..71b8a40a29 100644 --- a/esphome/components/ld2410/switch/__init__.py +++ b/esphome/components/ld2410/switch/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import switch import esphome.config_validation as cv from esphome.const import ( + CONF_BLUETOOTH, DEVICE_CLASS_SWITCH, ENTITY_CATEGORY_CONFIG, ICON_BLUETOOTH, @@ -14,7 +15,6 @@ BluetoothSwitch = ld2410_ns.class_("BluetoothSwitch", switch.Switch) EngineeringModeSwitch = ld2410_ns.class_("EngineeringModeSwitch", switch.Switch) CONF_ENGINEERING_MODE = "engineering_mode" -CONF_BLUETOOTH = "bluetooth" CONFIG_SCHEMA = { cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component), diff --git a/esphome/components/ld2420/ld2420.cpp b/esphome/components/ld2420/ld2420.cpp index 5b3206bf12..8a7d7de23b 100644 --- a/esphome/components/ld2420/ld2420.cpp +++ b/esphome/components/ld2420/ld2420.cpp @@ -1,4 +1,5 @@ #include "ld2420.h" +#include "esphome/core/application.h" #include "esphome/core/helpers.h" /* @@ -40,7 +41,7 @@ There are three documented parameters for modes: 00 04 = Energy output mode This mode outputs detailed signal energy values for each gate and the target distance. The data format consist of the following. - Header HH, Length LL, Persence PP, Distance DD, 16 Gate Energies EE, Footer FF + Header HH, Length LL, Presence PP, Distance DD, 16 Gate Energies EE, Footer FF HH HH HH HH LL LL PP DD DD EE EE .. 16x .. FF FF FF FF F4 F3 F2 F1 23 00 00 00 00 00 00 .. .. .. .. F8 F7 F6 F5 00 00 = debug output mode @@ -62,38 +63,105 @@ namespace ld2420 { static const char *const TAG = "ld2420"; -float LD2420Component::get_setup_priority() const { return setup_priority::BUS; } +// Local const's +static const uint16_t REFRESH_RATE_MS = 1000; -void LD2420Component::dump_config() { - ESP_LOGCONFIG(TAG, - "LD2420:\n" - " Firmware Version : %7s\n" - "LD2420 Number:", - this->ld2420_firmware_ver_); -#ifdef USE_NUMBER - LOG_NUMBER(TAG, " Gate Timeout:", this->gate_timeout_number_); - LOG_NUMBER(TAG, " Gate Max Distance:", this->max_gate_distance_number_); - LOG_NUMBER(TAG, " Gate Min Distance:", this->min_gate_distance_number_); - LOG_NUMBER(TAG, " Gate Select:", this->gate_select_number_); - for (uint8_t gate = 0; gate < LD2420_TOTAL_GATES; gate++) { - LOG_NUMBER(TAG, " Gate Move Threshold:", this->gate_move_threshold_numbers_[gate]); - LOG_NUMBER(TAG, " Gate Still Threshold::", this->gate_still_threshold_numbers_[gate]); - } -#endif -#ifdef USE_BUTTON - LOG_BUTTON(TAG, " Apply Config:", this->apply_config_button_); - LOG_BUTTON(TAG, " Revert Edits:", this->revert_config_button_); - LOG_BUTTON(TAG, " Factory Reset:", this->factory_reset_button_); - LOG_BUTTON(TAG, " Restart Module:", this->restart_module_button_); -#endif - ESP_LOGCONFIG(TAG, "LD2420 Select:"); - LOG_SELECT(TAG, " Operating Mode", this->operating_selector_); - if (this->get_firmware_int_(ld2420_firmware_ver_) < CALIBRATE_VERSION_MIN) { - ESP_LOGW(TAG, "LD2420 Firmware Version %s and older are only supported in Simple Mode", ld2420_firmware_ver_); +// Command sets +static const uint16_t CMD_DISABLE_CONF = 0x00FE; +static const uint16_t CMD_ENABLE_CONF = 0x00FF; +static const uint16_t CMD_PARM_HIGH_TRESH = 0x0012; +static const uint16_t CMD_PARM_LOW_TRESH = 0x0021; +static const uint16_t CMD_PROTOCOL_VER = 0x0002; +static const uint16_t CMD_READ_ABD_PARAM = 0x0008; +static const uint16_t CMD_READ_REG_ADDR = 0x0020; +static const uint16_t CMD_READ_REGISTER = 0x0002; +static const uint16_t CMD_READ_SERIAL_NUM = 0x0011; +static const uint16_t CMD_READ_SYS_PARAM = 0x0013; +static const uint16_t CMD_READ_VERSION = 0x0000; +static const uint16_t CMD_RESTART = 0x0068; +static const uint16_t CMD_SYSTEM_MODE = 0x0000; +static const uint16_t CMD_SYSTEM_MODE_GR = 0x0003; +static const uint16_t CMD_SYSTEM_MODE_MTT = 0x0001; +static const uint16_t CMD_SYSTEM_MODE_SIMPLE = 0x0064; +static const uint16_t CMD_SYSTEM_MODE_DEBUG = 0x0000; +static const uint16_t CMD_SYSTEM_MODE_ENERGY = 0x0004; +static const uint16_t CMD_SYSTEM_MODE_VS = 0x0002; +static const uint16_t CMD_WRITE_ABD_PARAM = 0x0007; +static const uint16_t CMD_WRITE_REGISTER = 0x0001; +static const uint16_t CMD_WRITE_SYS_PARAM = 0x0012; + +static const uint8_t CMD_ABD_DATA_REPLY_SIZE = 0x04; +static const uint8_t CMD_ABD_DATA_REPLY_START = 0x0A; +static const uint8_t CMD_MAX_BYTES = 0x64; +static const uint8_t CMD_REG_DATA_REPLY_SIZE = 0x02; + +static const uint8_t LD2420_ERROR_NONE = 0x00; +static const uint8_t LD2420_ERROR_TIMEOUT = 0x02; +static const uint8_t LD2420_ERROR_UNKNOWN = 0x01; + +// Register address values +static const uint16_t CMD_MIN_GATE_REG = 0x0000; +static const uint16_t CMD_MAX_GATE_REG = 0x0001; +static const uint16_t CMD_TIMEOUT_REG = 0x0004; +static const uint16_t CMD_GATE_MOVE_THRESH[TOTAL_GATES] = {0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, + 0x0016, 0x0017, 0x0018, 0x0019, 0x001A, 0x001B, + 0x001C, 0x001D, 0x001E, 0x001F}; +static const uint16_t CMD_GATE_STILL_THRESH[TOTAL_GATES] = {0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, + 0x0026, 0x0027, 0x0028, 0x0029, 0x002A, 0x002B, + 0x002C, 0x002D, 0x002E, 0x002F}; +static const uint32_t FACTORY_MOVE_THRESH[TOTAL_GATES] = {60000, 30000, 400, 250, 250, 250, 250, 250, + 250, 250, 250, 250, 250, 250, 250, 250}; +static const uint32_t FACTORY_STILL_THRESH[TOTAL_GATES] = {40000, 20000, 200, 200, 200, 200, 200, 150, + 150, 100, 100, 100, 100, 100, 100, 100}; +static const uint16_t FACTORY_TIMEOUT = 120; +static const uint16_t FACTORY_MIN_GATE = 1; +static const uint16_t FACTORY_MAX_GATE = 12; + +// COMMAND_BYTE Header & Footer +static const uint32_t CMD_FRAME_FOOTER = 0x01020304; +static const uint32_t CMD_FRAME_HEADER = 0xFAFBFCFD; +static const uint32_t DEBUG_FRAME_FOOTER = 0xFAFBFCFD; +static const uint32_t DEBUG_FRAME_HEADER = 0x1410BFAA; +static const uint32_t ENERGY_FRAME_FOOTER = 0xF5F6F7F8; +static const uint32_t ENERGY_FRAME_HEADER = 0xF1F2F3F4; +static const int CALIBRATE_VERSION_MIN = 154; +static const uint8_t CMD_FRAME_COMMAND = 6; +static const uint8_t CMD_FRAME_DATA_LENGTH = 4; +static const uint8_t CMD_FRAME_STATUS = 7; +static const uint8_t CMD_ERROR_WORD = 8; +static const uint8_t ENERGY_SENSOR_START = 9; +static const uint8_t CALIBRATE_REPORT_INTERVAL = 4; +static const std::string OP_NORMAL_MODE_STRING = "Normal"; +static const std::string OP_SIMPLE_MODE_STRING = "Simple"; + +// Memory-efficient lookup tables +struct StringToUint8 { + const char *str; + uint8_t value; +}; + +static constexpr StringToUint8 OP_MODE_BY_STR[] = { + {"Normal", OP_NORMAL_MODE}, + {"Calibrate", OP_CALIBRATE_MODE}, + {"Simple", OP_SIMPLE_MODE}, +}; + +static constexpr const char *ERR_MESSAGE[] = { + "None", + "Unknown", + "Timeout", +}; + +// Helper function for lookups +template uint8_t find_uint8(const StringToUint8 (&arr)[N], const std::string &str) { + for (const auto &entry : arr) { + if (str == entry.str) + return entry.value; } + return 0xFF; // Not found } -uint8_t LD2420Component::calc_checksum(void *data, size_t size) { +static uint8_t calc_checksum(void *data, size_t size) { uint8_t checksum = 0; uint8_t *data_bytes = (uint8_t *) data; for (size_t i = 0; i < size; i++) { @@ -102,7 +170,7 @@ uint8_t LD2420Component::calc_checksum(void *data, size_t size) { return checksum; } -int LD2420Component::get_firmware_int_(const char *version_string) { +static int get_firmware_int(const char *version_string) { std::string version_str = version_string; if (version_str[0] == 'v') { version_str = version_str.substr(1); @@ -112,10 +180,41 @@ int LD2420Component::get_firmware_int_(const char *version_string) { return version_integer; } +float LD2420Component::get_setup_priority() const { return setup_priority::BUS; } + +void LD2420Component::dump_config() { + ESP_LOGCONFIG(TAG, + "LD2420:\n" + " Firmware version: %7s", + this->firmware_ver_); +#ifdef USE_NUMBER + ESP_LOGCONFIG(TAG, "Number:"); + LOG_NUMBER(" ", "Gate Timeout:", this->gate_timeout_number_); + LOG_NUMBER(" ", "Gate Max Distance:", this->max_gate_distance_number_); + LOG_NUMBER(" ", "Gate Min Distance:", this->min_gate_distance_number_); + LOG_NUMBER(" ", "Gate Select:", this->gate_select_number_); + for (uint8_t gate = 0; gate < TOTAL_GATES; gate++) { + LOG_NUMBER(" ", "Gate Move Threshold:", this->gate_move_threshold_numbers_[gate]); + LOG_NUMBER(" ", "Gate Still Threshold::", this->gate_still_threshold_numbers_[gate]); + } +#endif +#ifdef USE_BUTTON + LOG_BUTTON(" ", "Apply Config:", this->apply_config_button_); + LOG_BUTTON(" ", "Revert Edits:", this->revert_config_button_); + LOG_BUTTON(" ", "Factory Reset:", this->factory_reset_button_); + LOG_BUTTON(" ", "Restart Module:", this->restart_module_button_); +#endif + ESP_LOGCONFIG(TAG, "Select:"); + LOG_SELECT(" ", "Operating Mode", this->operating_selector_); + if (ld2420::get_firmware_int(this->firmware_ver_) < CALIBRATE_VERSION_MIN) { + ESP_LOGW(TAG, "Firmware version %s and older supports Simple Mode only", this->firmware_ver_); + } +} + void LD2420Component::setup() { ESP_LOGCONFIG(TAG, "Running setup"); if (this->set_config_mode(true) == LD2420_ERROR_TIMEOUT) { - ESP_LOGE(TAG, "LD2420 module has failed to respond, check baud rate and serial connections."); + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); this->mark_failed(); return; } @@ -124,24 +223,24 @@ void LD2420Component::setup() { this->init_gate_config_numbers(); #endif this->get_firmware_version_(); - const char *pfw = this->ld2420_firmware_ver_; + const char *pfw = this->firmware_ver_; std::string fw_str(pfw); - for (auto &listener : listeners_) { + for (auto &listener : this->listeners_) { listener->on_fw_version(fw_str); } - for (uint8_t gate = 0; gate < LD2420_TOTAL_GATES; gate++) { + for (uint8_t gate = 0; gate < TOTAL_GATES; gate++) { delay_microseconds_safe(125); this->get_gate_threshold_(gate); } memcpy(&this->new_config, &this->current_config, sizeof(this->current_config)); - if (get_firmware_int_(ld2420_firmware_ver_) < CALIBRATE_VERSION_MIN) { + if (ld2420::get_firmware_int(this->firmware_ver_) < CALIBRATE_VERSION_MIN) { this->set_operating_mode(OP_SIMPLE_MODE_STRING); this->operating_selector_->publish_state(OP_SIMPLE_MODE_STRING); this->set_mode_(CMD_SYSTEM_MODE_SIMPLE); - ESP_LOGW(TAG, "LD2420 Frimware Version %s and older are only supported in Simple Mode", ld2420_firmware_ver_); + ESP_LOGW(TAG, "Firmware version %s and older supports Simple Mode only", this->firmware_ver_); } else { this->set_mode_(CMD_SYSTEM_MODE_ENERGY); this->operating_selector_->publish_state(OP_NORMAL_MODE_STRING); @@ -151,23 +250,22 @@ void LD2420Component::setup() { #endif this->set_system_mode(this->system_mode_); this->set_config_mode(false); - ESP_LOGCONFIG(TAG, "LD2420 setup complete."); } void LD2420Component::apply_config_action() { const uint8_t checksum = calc_checksum(&this->new_config, sizeof(this->new_config)); if (checksum == calc_checksum(&this->current_config, sizeof(this->current_config))) { - ESP_LOGCONFIG(TAG, "No configuration change detected"); + ESP_LOGD(TAG, "No configuration change detected"); return; } - ESP_LOGCONFIG(TAG, "Reconfiguring LD2420"); + ESP_LOGD(TAG, "Reconfiguring"); if (this->set_config_mode(true) == LD2420_ERROR_TIMEOUT) { - ESP_LOGE(TAG, "LD2420 module has failed to respond, check baud rate and serial connections."); + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); this->mark_failed(); return; } this->set_min_max_distances_timeout(this->new_config.max_gate, this->new_config.min_gate, this->new_config.timeout); - for (uint8_t gate = 0; gate < LD2420_TOTAL_GATES; gate++) { + for (uint8_t gate = 0; gate < TOTAL_GATES; gate++) { delay_microseconds_safe(125); this->set_gate_threshold(gate); } @@ -178,13 +276,12 @@ void LD2420Component::apply_config_action() { this->set_system_mode(this->system_mode_); this->set_config_mode(false); // Disable config mode to save new values in LD2420 nvm this->set_operating_mode(OP_NORMAL_MODE_STRING); - ESP_LOGCONFIG(TAG, "LD2420 reconfig complete."); } void LD2420Component::factory_reset_action() { - ESP_LOGCONFIG(TAG, "Setting factory defaults"); + ESP_LOGD(TAG, "Setting factory defaults"); if (this->set_config_mode(true) == LD2420_ERROR_TIMEOUT) { - ESP_LOGE(TAG, "LD2420 module has failed to respond, check baud rate and serial connections."); + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); this->mark_failed(); return; } @@ -194,7 +291,7 @@ void LD2420Component::factory_reset_action() { this->min_gate_distance_number_->state = FACTORY_MIN_GATE; this->max_gate_distance_number_->state = FACTORY_MAX_GATE; #endif - for (uint8_t gate = 0; gate < LD2420_TOTAL_GATES; gate++) { + for (uint8_t gate = 0; gate < TOTAL_GATES; gate++) { this->new_config.move_thresh[gate] = FACTORY_MOVE_THRESH[gate]; this->new_config.still_thresh[gate] = FACTORY_STILL_THRESH[gate]; delay_microseconds_safe(125); @@ -207,18 +304,16 @@ void LD2420Component::factory_reset_action() { this->init_gate_config_numbers(); this->refresh_gate_config_numbers(); #endif - ESP_LOGCONFIG(TAG, "LD2420 factory reset complete."); } void LD2420Component::restart_module_action() { - ESP_LOGCONFIG(TAG, "Restarting LD2420 module"); + ESP_LOGD(TAG, "Restarting"); this->send_module_restart(); this->set_timeout(250, [this]() { this->set_config_mode(true); - this->set_system_mode(system_mode_); + this->set_system_mode(this->system_mode_); this->set_config_mode(false); }); - ESP_LOGCONFIG(TAG, "LD2420 Restarted."); } void LD2420Component::revert_config_action() { @@ -226,25 +321,25 @@ void LD2420Component::revert_config_action() { #ifdef USE_NUMBER this->init_gate_config_numbers(); #endif - ESP_LOGCONFIG(TAG, "Reverted config number edits."); + ESP_LOGD(TAG, "Reverted config number edits"); } void LD2420Component::loop() { // If there is a active send command do not process it here, the send command call will handle it. - if (!get_cmd_active_()) { - if (!available()) + if (!this->get_cmd_active_()) { + if (!this->available()) return; static uint8_t buffer[2048]; static uint8_t rx_data; - while (available()) { - rx_data = read(); + while (this->available()) { + rx_data = this->read(); this->readline_(rx_data, buffer, sizeof(buffer)); } } } void LD2420Component::update_radar_data(uint16_t const *gate_energy, uint8_t sample_number) { - for (uint8_t gate = 0; gate < LD2420_TOTAL_GATES; ++gate) { + for (uint8_t gate = 0; gate < TOTAL_GATES; ++gate) { this->radar_data[gate][sample_number] = gate_energy[gate]; } this->total_sample_number_counter++; @@ -254,7 +349,7 @@ void LD2420Component::auto_calibrate_sensitivity() { // Calculate average and peak values for each gate const float move_factor = gate_move_sensitivity_factor + 1; const float still_factor = (gate_still_sensitivity_factor / 2) + 1; - for (uint8_t gate = 0; gate < LD2420_TOTAL_GATES; ++gate) { + for (uint8_t gate = 0; gate < TOTAL_GATES; ++gate) { uint32_t sum = 0; uint16_t peak = 0; @@ -283,7 +378,7 @@ void LD2420Component::auto_calibrate_sensitivity() { } void LD2420Component::report_gate_data() { - for (uint8_t gate = 0; gate < LD2420_TOTAL_GATES; ++gate) { + for (uint8_t gate = 0; gate < TOTAL_GATES; ++gate) { // Output results ESP_LOGI(TAG, "Gate: %2d Avg: %5d Peak: %5d", gate, this->gate_avg[gate], this->gate_peak[gate]); } @@ -292,13 +387,13 @@ void LD2420Component::report_gate_data() { void LD2420Component::set_operating_mode(const std::string &state) { // If unsupported firmware ignore mode select - if (get_firmware_int_(ld2420_firmware_ver_) >= CALIBRATE_VERSION_MIN) { - this->current_operating_mode = OP_MODE_TO_UINT.at(state); + if (ld2420::get_firmware_int(firmware_ver_) >= CALIBRATE_VERSION_MIN) { + this->current_operating_mode = find_uint8(OP_MODE_BY_STR, state); // Entering Auto Calibrate we need to clear the privoiuos data collection this->operating_selector_->publish_state(state); if (current_operating_mode == OP_CALIBRATE_MODE) { this->set_calibration_(true); - for (uint8_t gate = 0; gate < LD2420_TOTAL_GATES; gate++) { + for (uint8_t gate = 0; gate < TOTAL_GATES; gate++) { this->gate_avg[gate] = 0; this->gate_peak[gate] = 0; for (uint8_t i = 0; i < CALIBRATE_SAMPLES; i++) { @@ -333,11 +428,12 @@ void LD2420Component::readline_(int rx_data, uint8_t *buffer, int len) { this->set_cmd_active_(false); // Set command state to inactive after responce. this->handle_ack_data_(buffer, pos); pos = 0; - } else if ((buffer[pos - 2] == 0x0D && buffer[pos - 1] == 0x0A) && (get_mode_() == CMD_SYSTEM_MODE_SIMPLE)) { + } else if ((buffer[pos - 2] == 0x0D && buffer[pos - 1] == 0x0A) && + (this->get_mode_() == CMD_SYSTEM_MODE_SIMPLE)) { this->handle_simple_mode_(buffer, pos); pos = 0; } else if ((memcmp(&buffer[pos - 4], &ENERGY_FRAME_FOOTER, sizeof(ENERGY_FRAME_FOOTER)) == 0) && - (get_mode_() == CMD_SYSTEM_MODE_ENERGY)) { + (this->get_mode_() == CMD_SYSTEM_MODE_ENERGY)) { this->handle_energy_mode_(buffer, pos); pos = 0; } @@ -365,13 +461,13 @@ void LD2420Component::handle_energy_mode_(uint8_t *buffer, int len) { } // Resonable refresh rate for home assistant database size health - const int32_t current_millis = millis(); + const int32_t current_millis = App.get_loop_component_start_time(); if (current_millis - this->last_periodic_millis < REFRESH_RATE_MS) return; this->last_periodic_millis = current_millis; for (auto &listener : this->listeners_) { - listener->on_distance(get_distance_()); - listener->on_presence(get_presence_()); + listener->on_distance(this->get_distance_()); + listener->on_presence(this->get_presence_()); listener->on_energy(this->gate_energy_, sizeof(this->gate_energy_) / sizeof(this->gate_energy_[0])); } @@ -392,9 +488,9 @@ void LD2420Component::handle_simple_mode_(const uint8_t *inbuf, int len) { char outbuf[bufsize]{0}; while (true) { if (inbuf[pos - 2] == 'O' && inbuf[pos - 1] == 'F' && inbuf[pos] == 'F') { - set_presence_(false); + this->set_presence_(false); } else if (inbuf[pos - 1] == 'O' && inbuf[pos] == 'N') { - set_presence_(true); + this->set_presence_(true); } if (inbuf[pos] >= '0' && inbuf[pos] <= '9') { if (index < bufsize - 1) { @@ -411,18 +507,18 @@ void LD2420Component::handle_simple_mode_(const uint8_t *inbuf, int len) { } outbuf[index] = '\0'; if (index > 1) - set_distance_(strtol(outbuf, &endptr, 10)); + this->set_distance_(strtol(outbuf, &endptr, 10)); - if (get_mode_() == CMD_SYSTEM_MODE_SIMPLE) { + if (this->get_mode_() == CMD_SYSTEM_MODE_SIMPLE) { // Resonable refresh rate for home assistant database size health - const int32_t current_millis = millis(); + const int32_t current_millis = App.get_loop_component_start_time(); if (current_millis - this->last_normal_periodic_millis < REFRESH_RATE_MS) return; this->last_normal_periodic_millis = current_millis; for (auto &listener : this->listeners_) - listener->on_distance(get_distance_()); + listener->on_distance(this->get_distance_()); for (auto &listener : this->listeners_) - listener->on_presence(get_presence_()); + listener->on_presence(this->get_presence_()); } } @@ -433,10 +529,10 @@ void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) { uint8_t data_element = 0; uint16_t data_pos = 0; if (this->cmd_reply_.length > CMD_MAX_BYTES) { - ESP_LOGW(TAG, "LD2420 reply - received command reply frame is corrupt, length exceeds %d bytes.", CMD_MAX_BYTES); + ESP_LOGW(TAG, "Reply frame too long"); return; } else if (this->cmd_reply_.length < 2) { - ESP_LOGW(TAG, "LD2420 reply - received command frame is corrupt, length is less than 2 bytes."); + ESP_LOGW(TAG, "Command frame too short"); return; } memcpy(&this->cmd_reply_.error, &buffer[CMD_ERROR_WORD], sizeof(this->cmd_reply_.error)); @@ -447,13 +543,13 @@ void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) { this->cmd_reply_.ack = true; switch ((uint16_t) this->cmd_reply_.command) { case (CMD_ENABLE_CONF): - ESP_LOGD(TAG, "LD2420 reply - set config enable: CMD = %2X %s", CMD_ENABLE_CONF, result); + ESP_LOGV(TAG, "Set config enable: CMD = %2X %s", CMD_ENABLE_CONF, result); break; case (CMD_DISABLE_CONF): - ESP_LOGD(TAG, "LD2420 reply - set config disable: CMD = %2X %s", CMD_DISABLE_CONF, result); + ESP_LOGV(TAG, "Set config disable: CMD = %2X %s", CMD_DISABLE_CONF, result); break; case (CMD_READ_REGISTER): - ESP_LOGD(TAG, "LD2420 reply - read register: CMD = %2X %s", CMD_READ_REGISTER, result); + ESP_LOGV(TAG, "Read register: CMD = %2X %s", CMD_READ_REGISTER, result); // TODO Read/Write register is not implemented yet, this will get flushed out to a proper header file data_pos = 0x0A; for (uint16_t index = 0; index < (CMD_REG_DATA_REPLY_SIZE * // NOLINT @@ -465,13 +561,13 @@ void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) { } break; case (CMD_WRITE_REGISTER): - ESP_LOGD(TAG, "LD2420 reply - write register: CMD = %2X %s", CMD_WRITE_REGISTER, result); + ESP_LOGV(TAG, "Write register: CMD = %2X %s", CMD_WRITE_REGISTER, result); break; case (CMD_WRITE_ABD_PARAM): - ESP_LOGD(TAG, "LD2420 reply - write gate parameter(s): %2X %s", CMD_WRITE_ABD_PARAM, result); + ESP_LOGV(TAG, "Write gate parameter(s): %2X %s", CMD_WRITE_ABD_PARAM, result); break; case (CMD_READ_ABD_PARAM): - ESP_LOGD(TAG, "LD2420 reply - read gate parameter(s): %2X %s", CMD_READ_ABD_PARAM, result); + ESP_LOGV(TAG, "Read gate parameter(s): %2X %s", CMD_READ_ABD_PARAM, result); data_pos = CMD_ABD_DATA_REPLY_START; for (uint16_t index = 0; index < (CMD_ABD_DATA_REPLY_SIZE * // NOLINT ((buffer[CMD_FRAME_DATA_LENGTH] - 4) / CMD_ABD_DATA_REPLY_SIZE)); @@ -483,11 +579,11 @@ void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) { } break; case (CMD_WRITE_SYS_PARAM): - ESP_LOGD(TAG, "LD2420 reply - set system parameter(s): %2X %s", CMD_WRITE_SYS_PARAM, result); + ESP_LOGV(TAG, "Set system parameter(s): %2X %s", CMD_WRITE_SYS_PARAM, result); break; case (CMD_READ_VERSION): - memcpy(this->ld2420_firmware_ver_, &buffer[12], buffer[10]); - ESP_LOGD(TAG, "LD2420 reply - module firmware version: %7s %s", this->ld2420_firmware_ver_, result); + memcpy(this->firmware_ver_, &buffer[12], buffer[10]); + ESP_LOGV(TAG, "Firmware version: %7s %s", this->firmware_ver_, result); break; default: break; @@ -533,7 +629,7 @@ int LD2420Component::send_cmd_from_array(CmdFrameT frame) { } while (!this->cmd_reply_.ack) { - while (available()) { + while (this->available()) { this->readline_(read(), ack_buffer, sizeof(ack_buffer)); } delay_microseconds_safe(1450); @@ -548,7 +644,7 @@ int LD2420Component::send_cmd_from_array(CmdFrameT frame) { if (this->cmd_reply_.ack) retry = 0; if (this->cmd_reply_.error > 0) - handle_cmd_error(error); + this->handle_cmd_error(error); } return error; } @@ -563,7 +659,7 @@ uint8_t LD2420Component::set_config_mode(bool enable) { cmd_frame.data_length += sizeof(CMD_PROTOCOL_VER); } cmd_frame.footer = CMD_FRAME_FOOTER; - ESP_LOGD(TAG, "Sending set config %s command: %2X", enable ? "enable" : "disable", cmd_frame.command); + ESP_LOGV(TAG, "Sending set config %s command: %2X", enable ? "enable" : "disable", cmd_frame.command); return this->send_cmd_from_array(cmd_frame); } @@ -576,7 +672,7 @@ void LD2420Component::ld2420_restart() { cmd_frame.header = CMD_FRAME_HEADER; cmd_frame.command = CMD_RESTART; cmd_frame.footer = CMD_FRAME_FOOTER; - ESP_LOGD(TAG, "Sending restart command: %2X", cmd_frame.command); + ESP_LOGV(TAG, "Sending restart command: %2X", cmd_frame.command); this->send_cmd_from_array(cmd_frame); } @@ -588,7 +684,7 @@ void LD2420Component::get_reg_value_(uint16_t reg) { cmd_frame.data[1] = reg; cmd_frame.data_length += 2; cmd_frame.footer = CMD_FRAME_FOOTER; - ESP_LOGD(TAG, "Sending read register %4X command: %2X", reg, cmd_frame.command); + ESP_LOGV(TAG, "Sending read register %4X command: %2X", reg, cmd_frame.command); this->send_cmd_from_array(cmd_frame); } @@ -602,11 +698,11 @@ void LD2420Component::set_reg_value(uint16_t reg, uint16_t value) { memcpy(&cmd_frame.data[cmd_frame.data_length], &value, sizeof(CMD_REG_DATA_REPLY_SIZE)); cmd_frame.data_length += 2; cmd_frame.footer = CMD_FRAME_FOOTER; - ESP_LOGD(TAG, "Sending write register %4X command: %2X data = %4X", reg, cmd_frame.command, value); + ESP_LOGV(TAG, "Sending write register %4X command: %2X data = %4X", reg, cmd_frame.command, value); this->send_cmd_from_array(cmd_frame); } -void LD2420Component::handle_cmd_error(uint8_t error) { ESP_LOGI(TAG, "Command failed: %s", ERR_MESSAGE[error]); } +void LD2420Component::handle_cmd_error(uint8_t error) { ESP_LOGE(TAG, "Command failed: %s", ERR_MESSAGE[error]); } int LD2420Component::get_gate_threshold_(uint8_t gate) { uint8_t error; @@ -619,7 +715,7 @@ int LD2420Component::get_gate_threshold_(uint8_t gate) { memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_GATE_STILL_THRESH[gate], sizeof(CMD_GATE_STILL_THRESH[gate])); cmd_frame.data_length += 2; cmd_frame.footer = CMD_FRAME_FOOTER; - ESP_LOGD(TAG, "Sending read gate %d high/low theshold command: %2X", gate, cmd_frame.command); + ESP_LOGV(TAG, "Sending read gate %d high/low threshold command: %2X", gate, cmd_frame.command); error = this->send_cmd_from_array(cmd_frame); if (error == 0) { this->current_config.move_thresh[gate] = cmd_reply_.data[0]; @@ -644,7 +740,7 @@ int LD2420Component::get_min_max_distances_timeout_() { sizeof(CMD_TIMEOUT_REG)); // Register: global delay time cmd_frame.data_length += sizeof(CMD_TIMEOUT_REG); cmd_frame.footer = CMD_FRAME_FOOTER; - ESP_LOGD(TAG, "Sending read gate min max and timeout command: %2X", cmd_frame.command); + ESP_LOGV(TAG, "Sending read gate min max and timeout command: %2X", cmd_frame.command); error = this->send_cmd_from_array(cmd_frame); if (error == 0) { this->current_config.min_gate = (uint16_t) cmd_reply_.data[0]; @@ -667,9 +763,9 @@ void LD2420Component::set_system_mode(uint16_t mode) { memcpy(&cmd_frame.data[cmd_frame.data_length], &unknown_parm, sizeof(unknown_parm)); cmd_frame.data_length += sizeof(unknown_parm); cmd_frame.footer = CMD_FRAME_FOOTER; - ESP_LOGD(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) - set_mode_(mode); + this->set_mode_(mode); } void LD2420Component::get_firmware_version_() { @@ -679,7 +775,7 @@ void LD2420Component::get_firmware_version_() { cmd_frame.command = CMD_READ_VERSION; cmd_frame.footer = CMD_FRAME_FOOTER; - ESP_LOGD(TAG, "Sending read firmware version command: %2X", cmd_frame.command); + ESP_LOGV(TAG, "Sending read firmware version command: %2X", cmd_frame.command); this->send_cmd_from_array(cmd_frame); } @@ -712,7 +808,7 @@ void LD2420Component::set_min_max_distances_timeout(uint32_t max_gate_distance, cmd_frame.data_length += sizeof(timeout); cmd_frame.footer = CMD_FRAME_FOOTER; - ESP_LOGD(TAG, "Sending write gate min max and timeout command: %2X", cmd_frame.command); + ESP_LOGV(TAG, "Sending write gate min max and timeout command: %2X", cmd_frame.command); this->send_cmd_from_array(cmd_frame); } @@ -738,7 +834,7 @@ void LD2420Component::set_gate_threshold(uint8_t gate) { sizeof(this->new_config.still_thresh[gate])); cmd_frame.data_length += sizeof(this->new_config.still_thresh[gate]); cmd_frame.footer = CMD_FRAME_FOOTER; - ESP_LOGD(TAG, "Sending set gate %4X sensitivity command: %2X", gate, cmd_frame.command); + ESP_LOGV(TAG, "Sending set gate %4X sensitivity command: %2X", gate, cmd_frame.command); this->send_cmd_from_array(cmd_frame); } @@ -756,7 +852,7 @@ void LD2420Component::init_gate_config_numbers() { this->gate_move_sensitivity_factor_number_->publish_state(this->gate_move_sensitivity_factor); if (this->gate_still_sensitivity_factor_number_ != nullptr) this->gate_still_sensitivity_factor_number_->publish_state(this->gate_still_sensitivity_factor); - for (uint8_t gate = 0; gate < LD2420_TOTAL_GATES; gate++) { + for (uint8_t gate = 0; gate < TOTAL_GATES; gate++) { if (this->gate_still_threshold_numbers_[gate] != nullptr) { this->gate_still_threshold_numbers_[gate]->publish_state( static_cast(this->current_config.still_thresh[gate])); diff --git a/esphome/components/ld2420/ld2420.h b/esphome/components/ld2420/ld2420.h index 2b50c7a1d4..d574a25c89 100644 --- a/esphome/components/ld2420/ld2420.h +++ b/esphome/components/ld2420/ld2420.h @@ -16,88 +16,18 @@ #ifdef USE_BUTTON #include "esphome/components/button/button.h" #endif -#include -#include namespace esphome { namespace ld2420 { -// Local const's -static const uint16_t REFRESH_RATE_MS = 1000; - -// Command sets -static const uint8_t CMD_ABD_DATA_REPLY_SIZE = 0x04; -static const uint8_t CMD_ABD_DATA_REPLY_START = 0x0A; -static const uint16_t CMD_DISABLE_CONF = 0x00FE; -static const uint16_t CMD_ENABLE_CONF = 0x00FF; -static const uint8_t CMD_MAX_BYTES = 0x64; -static const uint16_t CMD_PARM_HIGH_TRESH = 0x0012; -static const uint16_t CMD_PARM_LOW_TRESH = 0x0021; -static const uint16_t CMD_PROTOCOL_VER = 0x0002; -static const uint16_t CMD_READ_ABD_PARAM = 0x0008; -static const uint16_t CMD_READ_REG_ADDR = 0x0020; -static const uint16_t CMD_READ_REGISTER = 0x0002; -static const uint16_t CMD_READ_SERIAL_NUM = 0x0011; -static const uint16_t CMD_READ_SYS_PARAM = 0x0013; -static const uint16_t CMD_READ_VERSION = 0x0000; -static const uint8_t CMD_REG_DATA_REPLY_SIZE = 0x02; -static const uint16_t CMD_RESTART = 0x0068; -static const uint16_t CMD_SYSTEM_MODE = 0x0000; -static const uint16_t CMD_SYSTEM_MODE_GR = 0x0003; -static const uint16_t CMD_SYSTEM_MODE_MTT = 0x0001; -static const uint16_t CMD_SYSTEM_MODE_SIMPLE = 0x0064; -static const uint16_t CMD_SYSTEM_MODE_DEBUG = 0x0000; -static const uint16_t CMD_SYSTEM_MODE_ENERGY = 0x0004; -static const uint16_t CMD_SYSTEM_MODE_VS = 0x0002; -static const uint16_t CMD_WRITE_ABD_PARAM = 0x0007; -static const uint16_t CMD_WRITE_REGISTER = 0x0001; -static const uint16_t CMD_WRITE_SYS_PARAM = 0x0012; - -static const uint8_t LD2420_ERROR_NONE = 0x00; -static const uint8_t LD2420_ERROR_TIMEOUT = 0x02; -static const uint8_t LD2420_ERROR_UNKNOWN = 0x01; -static const uint8_t LD2420_TOTAL_GATES = 16; +static const uint8_t TOTAL_GATES = 16; static const uint8_t CALIBRATE_SAMPLES = 64; -// Register address values -static const uint16_t CMD_MIN_GATE_REG = 0x0000; -static const uint16_t CMD_MAX_GATE_REG = 0x0001; -static const uint16_t CMD_TIMEOUT_REG = 0x0004; -static const uint16_t CMD_GATE_MOVE_THRESH[LD2420_TOTAL_GATES] = {0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, - 0x0016, 0x0017, 0x0018, 0x0019, 0x001A, 0x001B, - 0x001C, 0x001D, 0x001E, 0x001F}; -static const uint16_t CMD_GATE_STILL_THRESH[LD2420_TOTAL_GATES] = {0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, - 0x0026, 0x0027, 0x0028, 0x0029, 0x002A, 0x002B, - 0x002C, 0x002D, 0x002E, 0x002F}; -static const uint32_t FACTORY_MOVE_THRESH[LD2420_TOTAL_GATES] = {60000, 30000, 400, 250, 250, 250, 250, 250, - 250, 250, 250, 250, 250, 250, 250, 250}; -static const uint32_t FACTORY_STILL_THRESH[LD2420_TOTAL_GATES] = {40000, 20000, 200, 200, 200, 200, 200, 150, - 150, 100, 100, 100, 100, 100, 100, 100}; -static const uint16_t FACTORY_TIMEOUT = 120; -static const uint16_t FACTORY_MIN_GATE = 1; -static const uint16_t FACTORY_MAX_GATE = 12; - -// COMMAND_BYTE Header & Footer -static const uint8_t CMD_FRAME_COMMAND = 6; -static const uint8_t CMD_FRAME_DATA_LENGTH = 4; -static const uint32_t CMD_FRAME_FOOTER = 0x01020304; -static const uint32_t CMD_FRAME_HEADER = 0xFAFBFCFD; -static const uint32_t DEBUG_FRAME_FOOTER = 0xFAFBFCFD; -static const uint32_t DEBUG_FRAME_HEADER = 0x1410BFAA; -static const uint32_t ENERGY_FRAME_FOOTER = 0xF5F6F7F8; -static const uint32_t ENERGY_FRAME_HEADER = 0xF1F2F3F4; -static const uint8_t CMD_FRAME_STATUS = 7; -static const uint8_t CMD_ERROR_WORD = 8; -static const uint8_t ENERGY_SENSOR_START = 9; -static const uint8_t CALIBRATE_REPORT_INTERVAL = 4; -static const int CALIBRATE_VERSION_MIN = 154; -static const std::string OP_NORMAL_MODE_STRING = "Normal"; -static const std::string OP_SIMPLE_MODE_STRING = "Simple"; - -enum OpModeStruct : uint8_t { OP_NORMAL_MODE = 1, OP_CALIBRATE_MODE = 2, OP_SIMPLE_MODE = 3 }; -static const std::map OP_MODE_TO_UINT{ - {"Normal", OP_NORMAL_MODE}, {"Calibrate", OP_CALIBRATE_MODE}, {"Simple", OP_SIMPLE_MODE}}; -static constexpr const char *ERR_MESSAGE[] = {"None", "Unknown", "Timeout"}; +enum OpMode : uint8_t { + OP_NORMAL_MODE = 1, + OP_CALIBRATE_MODE = 2, + OP_SIMPLE_MODE = 3, +}; class LD2420Listener { public: @@ -109,6 +39,23 @@ class LD2420Listener { class LD2420Component : public Component, public uart::UARTDevice { public: + struct CmdFrameT { + uint32_t header{0}; + uint32_t footer{0}; + uint16_t length{0}; + uint16_t command{0}; + uint16_t data_length{0}; + uint8_t data[18]; + }; + + struct RegConfigT { + uint32_t move_thresh[TOTAL_GATES]; + uint32_t still_thresh[TOTAL_GATES]; + uint16_t min_gate{0}; + uint16_t max_gate{0}; + uint16_t timeout{0}; + }; + void setup() override; void dump_config() override; void loop() override; @@ -150,23 +97,6 @@ class LD2420Component : public Component, public uart::UARTDevice { #endif void register_listener(LD2420Listener *listener) { this->listeners_.push_back(listener); } - struct CmdFrameT { - uint32_t header{0}; - uint16_t length{0}; - uint16_t command{0}; - uint8_t data[18]; - uint16_t data_length{0}; - uint32_t footer{0}; - }; - - struct RegConfigT { - uint16_t min_gate{0}; - uint16_t max_gate{0}; - uint16_t timeout{0}; - uint32_t move_thresh[LD2420_TOTAL_GATES]; - uint32_t still_thresh[LD2420_TOTAL_GATES]; - }; - void send_module_restart(); void restart_module_action(); void apply_config_action(); @@ -179,23 +109,28 @@ class LD2420Component : public Component, public uart::UARTDevice { void set_operating_mode(const std::string &state); void auto_calibrate_sensitivity(); void update_radar_data(uint16_t const *gate_energy, uint8_t sample_number); - uint8_t calc_checksum(void *data, size_t size); + uint8_t set_config_mode(bool enable); + void set_min_max_distances_timeout(uint32_t max_gate_distance, uint32_t min_gate_distance, uint32_t timeout); + void set_gate_threshold(uint8_t gate); + void set_reg_value(uint16_t reg, uint16_t value); + void set_system_mode(uint16_t mode); + void ld2420_restart(); - RegConfigT current_config; - RegConfigT new_config; + float gate_move_sensitivity_factor{0.5}; + float gate_still_sensitivity_factor{0.5}; int32_t last_periodic_millis = millis(); int32_t report_periodic_millis = millis(); int32_t monitor_periodic_millis = millis(); int32_t last_normal_periodic_millis = millis(); - bool output_energy_state{false}; - uint8_t current_operating_mode{OP_NORMAL_MODE}; - uint16_t radar_data[LD2420_TOTAL_GATES][CALIBRATE_SAMPLES]; - uint16_t gate_avg[LD2420_TOTAL_GATES]; - uint16_t gate_peak[LD2420_TOTAL_GATES]; - uint8_t sample_number_counter{0}; + uint16_t radar_data[TOTAL_GATES][CALIBRATE_SAMPLES]; + uint16_t gate_avg[TOTAL_GATES]; + uint16_t gate_peak[TOTAL_GATES]; uint16_t total_sample_number_counter{0}; - float gate_move_sensitivity_factor{0.5}; - float gate_still_sensitivity_factor{0.5}; + uint8_t current_operating_mode{OP_NORMAL_MODE}; + uint8_t sample_number_counter{0}; + bool output_energy_state{false}; + RegConfigT current_config; + RegConfigT new_config; #ifdef USE_SELECT select::Select *operating_selector_{nullptr}; #endif @@ -205,24 +140,17 @@ class LD2420Component : public Component, public uart::UARTDevice { button::Button *restart_module_button_{nullptr}; button::Button *factory_reset_button_{nullptr}; #endif - void set_min_max_distances_timeout(uint32_t max_gate_distance, uint32_t min_gate_distance, uint32_t timeout); - void set_gate_threshold(uint8_t gate); - void set_reg_value(uint16_t reg, uint16_t value); - uint8_t set_config_mode(bool enable); - void set_system_mode(uint16_t mode); - void ld2420_restart(); protected: struct CmdReplyT { + uint32_t data[4]; + uint16_t error; uint8_t command; uint8_t status; - uint32_t data[4]; uint8_t length; - uint16_t error; volatile bool ack; }; - int get_firmware_int_(const char *version_string); void get_firmware_version_(); int get_gate_threshold_(uint8_t gate); void get_reg_value_(uint16_t reg); @@ -253,17 +181,17 @@ class LD2420Component : public Component, public uart::UARTDevice { std::vector gate_move_threshold_numbers_ = std::vector(16); #endif - uint16_t gate_energy_[LD2420_TOTAL_GATES]; - CmdReplyT cmd_reply_; uint32_t max_distance_gate_; uint32_t min_distance_gate_; - uint16_t system_mode_{CMD_SYSTEM_MODE_ENERGY}; - bool cmd_active_{false}; - char ld2420_firmware_ver_[8]{"v0.0.0"}; - bool presence_{false}; - bool calibration_{false}; + uint16_t system_mode_; + uint16_t gate_energy_[TOTAL_GATES]; uint16_t distance_{0}; uint8_t config_checksum_{0}; + char firmware_ver_[8]{"v0.0.0"}; + bool cmd_active_{false}; + bool presence_{false}; + bool calibration_{false}; + CmdReplyT cmd_reply_; std::vector listeners_{}; }; diff --git a/esphome/components/ld2420/number/__init__.py b/esphome/components/ld2420/number/__init__.py index 1558243cc2..a2637b7b06 100644 --- a/esphome/components/ld2420/number/__init__.py +++ b/esphome/components/ld2420/number/__init__.py @@ -3,6 +3,8 @@ from esphome.components import number import esphome.config_validation as cv from esphome.const import ( CONF_ID, + CONF_MOVE_THRESHOLD, + CONF_STILL_THRESHOLD, DEVICE_CLASS_DISTANCE, ENTITY_CATEGORY_CONFIG, ICON_MOTION_SENSOR, @@ -31,8 +33,6 @@ LD2420StillThresholdNumbers = ld2420_ns.class_( ) CONF_MIN_GATE_DISTANCE = "min_gate_distance" CONF_MAX_GATE_DISTANCE = "max_gate_distance" -CONF_STILL_THRESHOLD = "still_threshold" -CONF_MOVE_THRESHOLD = "move_threshold" CONF_GATE_MOVE_SENSITIVITY = "gate_move_sensitivity" CONF_GATE_STILL_SENSITIVITY = "gate_still_sensitivity" CONF_GATE_SELECT = "gate_select" diff --git a/esphome/components/ld2420/sensor/__init__.py b/esphome/components/ld2420/sensor/__init__.py index e39ca99ae1..6dde35753a 100644 --- a/esphome/components/ld2420/sensor/__init__.py +++ b/esphome/components/ld2420/sensor/__init__.py @@ -1,13 +1,17 @@ import esphome.codegen as cg from esphome.components import sensor import esphome.config_validation as cv -from esphome.const import CONF_ID, DEVICE_CLASS_DISTANCE, UNIT_CENTIMETER +from esphome.const import ( + CONF_ID, + CONF_MOVING_DISTANCE, + DEVICE_CLASS_DISTANCE, + UNIT_CENTIMETER, +) from .. import CONF_LD2420_ID, LD2420Component, ld2420_ns LD2420Sensor = ld2420_ns.class_("LD2420Sensor", sensor.Sensor, cg.Component) -CONF_MOVING_DISTANCE = "moving_distance" CONF_GATE_ENERGY = "gate_energy" CONFIG_SCHEMA = cv.All( diff --git a/esphome/components/ld2420/sensor/ld2420_sensor.h b/esphome/components/ld2420/sensor/ld2420_sensor.h index 4eebefe0e3..82730d60e3 100644 --- a/esphome/components/ld2420/sensor/ld2420_sensor.h +++ b/esphome/components/ld2420/sensor/ld2420_sensor.h @@ -27,7 +27,7 @@ class LD2420Sensor : public LD2420Listener, public Component, sensor::Sensor { protected: sensor::Sensor *distance_sensor_{nullptr}; - std::vector energy_sensors_ = std::vector(LD2420_TOTAL_GATES); + std::vector energy_sensors_ = std::vector(TOTAL_GATES); }; } // namespace ld2420 diff --git a/esphome/components/ld2450/button/__init__.py b/esphome/components/ld2450/button/__init__.py index 39671d3a3b..429aa59389 100644 --- a/esphome/components/ld2450/button/__init__.py +++ b/esphome/components/ld2450/button/__init__.py @@ -13,13 +13,13 @@ from esphome.const import ( from .. import CONF_LD2450_ID, LD2450Component, ld2450_ns -ResetButton = ld2450_ns.class_("ResetButton", button.Button) +FactoryResetButton = ld2450_ns.class_("FactoryResetButton", button.Button) RestartButton = ld2450_ns.class_("RestartButton", button.Button) CONFIG_SCHEMA = { cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component), cv.Optional(CONF_FACTORY_RESET): button.button_schema( - ResetButton, + FactoryResetButton, device_class=DEVICE_CLASS_RESTART, entity_category=ENTITY_CATEGORY_CONFIG, icon=ICON_RESTART_ALERT, @@ -38,7 +38,7 @@ async def to_code(config): if factory_reset_config := config.get(CONF_FACTORY_RESET): b = await button.new_button(factory_reset_config) await cg.register_parented(b, config[CONF_LD2450_ID]) - cg.add(ld2450_component.set_reset_button(b)) + cg.add(ld2450_component.set_factory_reset_button(b)) if restart_config := config.get(CONF_RESTART): b = await button.new_button(restart_config) await cg.register_parented(b, config[CONF_LD2450_ID]) diff --git a/esphome/components/ld2450/button/factory_reset_button.cpp b/esphome/components/ld2450/button/factory_reset_button.cpp new file mode 100644 index 0000000000..bcac7ada2f --- /dev/null +++ b/esphome/components/ld2450/button/factory_reset_button.cpp @@ -0,0 +1,9 @@ +#include "factory_reset_button.h" + +namespace esphome { +namespace ld2450 { + +void FactoryResetButton::press_action() { this->parent_->factory_reset(); } + +} // namespace ld2450 +} // namespace esphome diff --git a/esphome/components/ld2450/button/reset_button.h b/esphome/components/ld2450/button/factory_reset_button.h similarity index 65% rename from esphome/components/ld2450/button/reset_button.h rename to esphome/components/ld2450/button/factory_reset_button.h index 73804fa6d6..8e80347119 100644 --- a/esphome/components/ld2450/button/reset_button.h +++ b/esphome/components/ld2450/button/factory_reset_button.h @@ -6,9 +6,9 @@ namespace esphome { namespace ld2450 { -class ResetButton : public button::Button, public Parented { +class FactoryResetButton : public button::Button, public Parented { public: - ResetButton() = default; + FactoryResetButton() = default; protected: void press_action() override; diff --git a/esphome/components/ld2450/button/reset_button.cpp b/esphome/components/ld2450/button/reset_button.cpp deleted file mode 100644 index e96ec99cc5..0000000000 --- a/esphome/components/ld2450/button/reset_button.cpp +++ /dev/null @@ -1,9 +0,0 @@ -#include "reset_button.h" - -namespace esphome { -namespace ld2450 { - -void ResetButton::press_action() { this->parent_->factory_reset(); } - -} // namespace ld2450 -} // namespace esphome diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index 519e4d89a3..8f3b3a3f21 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -1,12 +1,15 @@ #include "ld2450.h" #include +#include #ifdef USE_NUMBER #include "esphome/components/number/number.h" #endif #ifdef USE_SENSOR #include "esphome/components/sensor/sensor.h" #endif +#include "esphome/core/application.h" #include "esphome/core/component.h" +#include "esphome/core/helpers.h" #define highbyte(val) (uint8_t)((val) >> 8) #define lowbyte(val) (uint8_t)((val) &0xff) @@ -15,36 +18,129 @@ namespace esphome { namespace ld2450 { static const char *const TAG = "ld2450"; -static const char *const NO_MAC("08:05:04:03:02:01"); -static const char *const UNKNOWN_MAC("unknown"); +static const char *const UNKNOWN_MAC = "unknown"; +static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X"; + +enum BaudRate : uint8_t { + BAUD_RATE_9600 = 1, + BAUD_RATE_19200 = 2, + BAUD_RATE_38400 = 3, + BAUD_RATE_57600 = 4, + BAUD_RATE_115200 = 5, + BAUD_RATE_230400 = 6, + BAUD_RATE_256000 = 7, + BAUD_RATE_460800 = 8 +}; + +enum ZoneType : uint8_t { + ZONE_DISABLED = 0, + ZONE_DETECTION = 1, + ZONE_FILTER = 2, +}; + +enum PeriodicData : uint8_t { + TARGET_X = 4, + TARGET_Y = 6, + TARGET_SPEED = 8, + TARGET_RESOLUTION = 10, +}; + +enum PeriodicDataValue : uint8_t { + HEADER = 0xAA, + FOOTER = 0x55, + CHECK = 0x00, +}; + +enum AckData : uint8_t { + COMMAND = 6, + COMMAND_STATUS = 7, +}; + +// Memory-efficient lookup tables +struct StringToUint8 { + const char *str; + const uint8_t value; +}; + +struct Uint8ToString { + const uint8_t value; + const char *str; +}; + +constexpr StringToUint8 BAUD_RATES_BY_STR[] = { + {"9600", BAUD_RATE_9600}, {"19200", BAUD_RATE_19200}, {"38400", BAUD_RATE_38400}, + {"57600", BAUD_RATE_57600}, {"115200", BAUD_RATE_115200}, {"230400", BAUD_RATE_230400}, + {"256000", BAUD_RATE_256000}, {"460800", BAUD_RATE_460800}, +}; + +constexpr Uint8ToString DIRECTION_BY_UINT[] = { + {DIRECTION_APPROACHING, "Approaching"}, + {DIRECTION_MOVING_AWAY, "Moving away"}, + {DIRECTION_STATIONARY, "Stationary"}, + {DIRECTION_NA, "NA"}, +}; + +constexpr Uint8ToString ZONE_TYPE_BY_UINT[] = { + {ZONE_DISABLED, "Disabled"}, + {ZONE_DETECTION, "Detection"}, + {ZONE_FILTER, "Filter"}, +}; + +constexpr StringToUint8 ZONE_TYPE_BY_STR[] = { + {"Disabled", ZONE_DISABLED}, + {"Detection", ZONE_DETECTION}, + {"Filter", ZONE_FILTER}, +}; + +// Helper functions for lookups +template uint8_t find_uint8(const StringToUint8 (&arr)[N], const std::string &str) { + for (const auto &entry : arr) { + if (str == entry.str) + return entry.value; + } + return 0xFF; // Not found +} + +template const char *find_str(const Uint8ToString (&arr)[N], uint8_t value) { + for (const auto &entry : arr) { + if (value == entry.value) + return entry.str; + } + return ""; // Not found +} // LD2450 UART Serial Commands -static const uint8_t CMD_ENABLE_CONF = 0x00FF; -static const uint8_t CMD_DISABLE_CONF = 0x00FE; -static const uint8_t CMD_VERSION = 0x00A0; -static const uint8_t CMD_MAC = 0x00A5; -static const uint8_t CMD_RESET = 0x00A2; -static const uint8_t CMD_RESTART = 0x00A3; -static const uint8_t CMD_BLUETOOTH = 0x00A4; -static const uint8_t CMD_SINGLE_TARGET_MODE = 0x0080; -static const uint8_t CMD_MULTI_TARGET_MODE = 0x0090; -static const uint8_t CMD_QUERY_TARGET_MODE = 0x0091; -static const uint8_t CMD_SET_BAUD_RATE = 0x00A1; -static const uint8_t CMD_QUERY_ZONE = 0x00C1; -static const uint8_t CMD_SET_ZONE = 0x00C2; +static constexpr uint8_t CMD_ENABLE_CONF = 0xFF; +static constexpr uint8_t CMD_DISABLE_CONF = 0xFE; +static constexpr uint8_t CMD_QUERY_VERSION = 0xA0; +static constexpr uint8_t CMD_QUERY_MAC_ADDRESS = 0xA5; +static constexpr uint8_t CMD_RESET = 0xA2; +static constexpr uint8_t CMD_RESTART = 0xA3; +static constexpr uint8_t CMD_BLUETOOTH = 0xA4; +static constexpr uint8_t CMD_SINGLE_TARGET_MODE = 0x80; +static constexpr uint8_t CMD_MULTI_TARGET_MODE = 0x90; +static constexpr uint8_t CMD_QUERY_TARGET_MODE = 0x91; +static constexpr uint8_t CMD_SET_BAUD_RATE = 0xA1; +static constexpr uint8_t CMD_QUERY_ZONE = 0xC1; +static constexpr uint8_t CMD_SET_ZONE = 0xC2; +// Header & Footer size +static constexpr uint8_t HEADER_FOOTER_SIZE = 4; +// Command Header & Footer +static constexpr uint8_t CMD_FRAME_HEADER[HEADER_FOOTER_SIZE] = {0xFD, 0xFC, 0xFB, 0xFA}; +static constexpr uint8_t CMD_FRAME_FOOTER[HEADER_FOOTER_SIZE] = {0x04, 0x03, 0x02, 0x01}; +// Data Header & Footer +static constexpr uint8_t DATA_FRAME_HEADER[HEADER_FOOTER_SIZE] = {0xAA, 0xFF, 0x03, 0x00}; +static constexpr uint8_t DATA_FRAME_FOOTER[2] = {0x55, 0xCC}; +// MAC address the module uses when Bluetooth is disabled +static constexpr uint8_t NO_MAC[] = {0x08, 0x05, 0x04, 0x03, 0x02, 0x01}; static inline uint16_t convert_seconds_to_ms(uint16_t value) { return value * 1000; }; -static inline std::string convert_signed_int_to_hex(int value) { - auto value_as_str = str_snprintf("%04x", 4, value & 0xFFFF); - return value_as_str; -} - static inline void convert_int_values_to_hex(const int *values, uint8_t *bytes) { - for (int i = 0; i < 4; i++) { - std::string temp_hex = convert_signed_int_to_hex(values[i]); - bytes[i * 2] = std::stoi(temp_hex.substr(2, 2), nullptr, 16); // Store high byte - bytes[i * 2 + 1] = std::stoi(temp_hex.substr(0, 2), nullptr, 16); // Store low byte + for (uint8_t i = 0; i < 4; i++) { + uint16_t val = values[i] & 0xFFFF; + bytes[i * 2] = val & 0xFF; // Store low byte first (little-endian) + bytes[i * 2 + 1] = (val >> 8) & 0xFF; // Store high byte second } } @@ -82,32 +178,15 @@ static inline float calculate_angle(float base, float hypotenuse) { return angle_degrees; } -static inline std::string get_direction(int16_t speed) { - static const char *const APPROACHING = "Approaching"; - static const char *const MOVING_AWAY = "Moving away"; - static const char *const STATIONARY = "Stationary"; - - if (speed > 0) { - return MOVING_AWAY; +static bool validate_header_footer(const uint8_t *header_footer, const uint8_t *buffer) { + for (uint8_t i = 0; i < HEADER_FOOTER_SIZE; i++) { + if (header_footer[i] != buffer[i]) { + return false; // Mismatch in header/footer + } } - if (speed < 0) { - return APPROACHING; - } - return STATIONARY; + return true; // Valid header/footer } -static inline std::string format_mac(uint8_t *buffer) { - return str_snprintf("%02X:%02X:%02X:%02X:%02X:%02X", 17, buffer[10], buffer[11], buffer[12], buffer[13], buffer[14], - buffer[15]); -} - -static inline std::string format_version(uint8_t *buffer) { - return str_sprintf("%u.%02X.%02X%02X%02X%02X", buffer[13], buffer[12], buffer[17], buffer[16], buffer[15], - buffer[14]); -} - -LD2450Component::LD2450Component() {} - void LD2450Component::setup() { ESP_LOGCONFIG(TAG, "Running setup"); #ifdef USE_NUMBER @@ -120,84 +199,93 @@ void LD2450Component::setup() { } void LD2450Component::dump_config() { - ESP_LOGCONFIG(TAG, "HLK-LD2450 Human motion tracking radar module:"); + std::string mac_str = + mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC; + std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5], + this->version_[4], this->version_[3], this->version_[2]); + ESP_LOGCONFIG(TAG, + "LD2450:\n" + " Firmware version: %s\n" + " MAC address: %s\n" + " Throttle: %u ms", + version.c_str(), mac_str.c_str(), this->throttle_); #ifdef USE_BINARY_SENSOR - LOG_BINARY_SENSOR(" ", "TargetBinarySensor", this->target_binary_sensor_); - LOG_BINARY_SENSOR(" ", "MovingTargetBinarySensor", this->moving_target_binary_sensor_); - LOG_BINARY_SENSOR(" ", "StillTargetBinarySensor", this->still_target_binary_sensor_); -#endif -#ifdef USE_SWITCH - LOG_SWITCH(" ", "BluetoothSwitch", this->bluetooth_switch_); - LOG_SWITCH(" ", "MultiTargetSwitch", this->multi_target_switch_); -#endif -#ifdef USE_BUTTON - LOG_BUTTON(" ", "ResetButton", this->reset_button_); - LOG_BUTTON(" ", "RestartButton", this->restart_button_); + ESP_LOGCONFIG(TAG, "Binary Sensors:"); + LOG_BINARY_SENSOR(" ", "MovingTarget", this->moving_target_binary_sensor_); + LOG_BINARY_SENSOR(" ", "StillTarget", this->still_target_binary_sensor_); + LOG_BINARY_SENSOR(" ", "Target", this->target_binary_sensor_); #endif #ifdef USE_SENSOR - LOG_SENSOR(" ", "TargetCountSensor", this->target_count_sensor_); - LOG_SENSOR(" ", "StillTargetCountSensor", this->still_target_count_sensor_); - LOG_SENSOR(" ", "MovingTargetCountSensor", this->moving_target_count_sensor_); + ESP_LOGCONFIG(TAG, "Sensors:"); + LOG_SENSOR(" ", "MovingTargetCount", this->moving_target_count_sensor_); + LOG_SENSOR(" ", "StillTargetCount", this->still_target_count_sensor_); + LOG_SENSOR(" ", "TargetCount", this->target_count_sensor_); for (sensor::Sensor *s : this->move_x_sensors_) { - LOG_SENSOR(" ", "NthTargetXSensor", s); + LOG_SENSOR(" ", "TargetX", s); } for (sensor::Sensor *s : this->move_y_sensors_) { - LOG_SENSOR(" ", "NthTargetYSensor", s); - } - for (sensor::Sensor *s : this->move_speed_sensors_) { - LOG_SENSOR(" ", "NthTargetSpeedSensor", s); + LOG_SENSOR(" ", "TargetY", s); } for (sensor::Sensor *s : this->move_angle_sensors_) { - LOG_SENSOR(" ", "NthTargetAngleSensor", s); + LOG_SENSOR(" ", "TargetAngle", s); } for (sensor::Sensor *s : this->move_distance_sensors_) { - LOG_SENSOR(" ", "NthTargetDistanceSensor", s); + LOG_SENSOR(" ", "TargetDistance", s); } for (sensor::Sensor *s : this->move_resolution_sensors_) { - LOG_SENSOR(" ", "NthTargetResolutionSensor", s); + LOG_SENSOR(" ", "TargetResolution", s); + } + for (sensor::Sensor *s : this->move_speed_sensors_) { + LOG_SENSOR(" ", "TargetSpeed", s); } for (sensor::Sensor *s : this->zone_target_count_sensors_) { - LOG_SENSOR(" ", "NthZoneTargetCountSensor", s); - } - for (sensor::Sensor *s : this->zone_still_target_count_sensors_) { - LOG_SENSOR(" ", "NthZoneStillTargetCountSensor", s); + LOG_SENSOR(" ", "ZoneTargetCount", s); } for (sensor::Sensor *s : this->zone_moving_target_count_sensors_) { - LOG_SENSOR(" ", "NthZoneMovingTargetCountSensor", s); + LOG_SENSOR(" ", "ZoneMovingTargetCount", s); + } + for (sensor::Sensor *s : this->zone_still_target_count_sensors_) { + LOG_SENSOR(" ", "ZoneStillTargetCount", s); } #endif #ifdef USE_TEXT_SENSOR - LOG_TEXT_SENSOR(" ", "VersionTextSensor", this->version_text_sensor_); - LOG_TEXT_SENSOR(" ", "MacTextSensor", this->mac_text_sensor_); + ESP_LOGCONFIG(TAG, "Text Sensors:"); + LOG_TEXT_SENSOR(" ", "Version", this->version_text_sensor_); + LOG_TEXT_SENSOR(" ", "Mac", this->mac_text_sensor_); for (text_sensor::TextSensor *s : this->direction_text_sensors_) { - LOG_TEXT_SENSOR(" ", "NthDirectionTextSensor", s); + LOG_TEXT_SENSOR(" ", "Direction", s); } #endif #ifdef USE_NUMBER + ESP_LOGCONFIG(TAG, "Numbers:"); + LOG_NUMBER(" ", "PresenceTimeout", this->presence_timeout_number_); for (auto n : this->zone_numbers_) { - LOG_NUMBER(" ", "ZoneX1Number", n.x1); - LOG_NUMBER(" ", "ZoneY1Number", n.y1); - LOG_NUMBER(" ", "ZoneX2Number", n.x2); - LOG_NUMBER(" ", "ZoneY2Number", n.y2); + LOG_NUMBER(" ", "ZoneX1", n.x1); + LOG_NUMBER(" ", "ZoneY1", n.y1); + LOG_NUMBER(" ", "ZoneX2", n.x2); + LOG_NUMBER(" ", "ZoneY2", n.y2); } #endif #ifdef USE_SELECT - LOG_SELECT(" ", "BaudRateSelect", this->baud_rate_select_); - LOG_SELECT(" ", "ZoneTypeSelect", this->zone_type_select_); + ESP_LOGCONFIG(TAG, "Selects:"); + LOG_SELECT(" ", "BaudRate", this->baud_rate_select_); + LOG_SELECT(" ", "ZoneType", this->zone_type_select_); #endif -#ifdef USE_NUMBER - LOG_NUMBER(" ", "PresenceTimeoutNumber", this->presence_timeout_number_); +#ifdef USE_SWITCH + ESP_LOGCONFIG(TAG, "Switches:"); + LOG_SWITCH(" ", "Bluetooth", this->bluetooth_switch_); + LOG_SWITCH(" ", "MultiTarget", this->multi_target_switch_); +#endif +#ifdef USE_BUTTON + ESP_LOGCONFIG(TAG, "Buttons:"); + LOG_BUTTON(" ", "FactoryReset", this->factory_reset_button_); + LOG_BUTTON(" ", "Restart", this->restart_button_); #endif - ESP_LOGCONFIG(TAG, - " Throttle : %ums\n" - " MAC Address : %s\n" - " Firmware version : %s", - this->throttle_, const_cast(this->mac_.c_str()), const_cast(this->version_.c_str())); } void LD2450Component::loop() { while (this->available()) { - this->readline_(read(), this->buffer_data_, MAX_LINE_LENGTH); + this->readline_(this->read()); } } @@ -232,7 +320,7 @@ void LD2450Component::set_radar_zone(int32_t zone_type, int32_t zone1_x1, int32_ this->zone_type_ = zone_type; int zone_parameters[12] = {zone1_x1, zone1_y1, zone1_x2, zone1_y2, zone2_x1, zone2_y1, zone2_x2, zone2_y2, zone3_x1, zone3_y1, zone3_x2, zone3_y2}; - for (int i = 0; i < MAX_ZONES; i++) { + for (uint8_t i = 0; i < MAX_ZONES; i++) { this->zone_config_[i].x1 = zone_parameters[i * 4]; this->zone_config_[i].y1 = zone_parameters[i * 4 + 1]; this->zone_config_[i].x2 = zone_parameters[i * 4 + 2]; @@ -246,15 +334,15 @@ void LD2450Component::send_set_zone_command_() { uint8_t cmd_value[26] = {}; uint8_t zone_type_bytes[2] = {static_cast(this->zone_type_), 0x00}; uint8_t area_config[24] = {}; - for (int i = 0; i < MAX_ZONES; i++) { + for (uint8_t i = 0; i < MAX_ZONES; i++) { int values[4] = {this->zone_config_[i].x1, this->zone_config_[i].y1, this->zone_config_[i].x2, this->zone_config_[i].y2}; ld2450::convert_int_values_to_hex(values, area_config + (i * 8)); } - std::memcpy(cmd_value, zone_type_bytes, 2); - std::memcpy(cmd_value + 2, area_config, 24); + std::memcpy(cmd_value, zone_type_bytes, sizeof(zone_type_bytes)); + std::memcpy(cmd_value + 2, area_config, sizeof(area_config)); this->set_config_mode_(true); - this->send_command_(CMD_SET_ZONE, cmd_value, 26); + this->send_command_(CMD_SET_ZONE, cmd_value, sizeof(cmd_value)); this->set_config_mode_(false); } @@ -266,19 +354,18 @@ bool LD2450Component::get_timeout_status_(uint32_t check_millis) { if (this->timeout_ == 0) { this->timeout_ = ld2450::convert_seconds_to_ms(DEFAULT_PRESENCE_TIMEOUT); } - auto current_millis = millis(); - return current_millis - check_millis >= this->timeout_; + return App.get_loop_component_start_time() - check_millis >= this->timeout_; } // Extract, store and publish zone details LD2450 buffer -void LD2450Component::process_zone_(uint8_t *buffer) { +void LD2450Component::process_zone_() { uint8_t index, start; for (index = 0; index < MAX_ZONES; index++) { start = 12 + index * 8; - this->zone_config_[index].x1 = ld2450::hex_to_signed_int(buffer, start); - this->zone_config_[index].y1 = ld2450::hex_to_signed_int(buffer, start + 2); - this->zone_config_[index].x2 = ld2450::hex_to_signed_int(buffer, start + 4); - this->zone_config_[index].y2 = ld2450::hex_to_signed_int(buffer, start + 6); + this->zone_config_[index].x1 = ld2450::hex_to_signed_int(this->buffer_data_, start); + this->zone_config_[index].y1 = ld2450::hex_to_signed_int(this->buffer_data_, start + 2); + this->zone_config_[index].x2 = ld2450::hex_to_signed_int(this->buffer_data_, start + 4); + this->zone_config_[index].y2 = ld2450::hex_to_signed_int(this->buffer_data_, start + 6); #ifdef USE_NUMBER // only one null check as all coordinates are required for a single zone if (this->zone_numbers_[index].x1 != nullptr) { @@ -324,27 +411,25 @@ void LD2450Component::restart_and_read_all_info() { // Send command with values to LD2450 void LD2450Component::send_command_(uint8_t command, const uint8_t *command_value, uint8_t command_value_len) { - ESP_LOGV(TAG, "Sending command %02X", command); - // frame header - this->write_array(CMD_FRAME_HEADER, 4); + ESP_LOGV(TAG, "Sending COMMAND %02X", command); + // frame header bytes + this->write_array(CMD_FRAME_HEADER, sizeof(CMD_FRAME_HEADER)); // length bytes - int len = 2; + uint8_t len = 2; if (command_value != nullptr) { len += command_value_len; } - this->write_byte(lowbyte(len)); - this->write_byte(highbyte(len)); - // command - this->write_byte(lowbyte(command)); - this->write_byte(highbyte(command)); + uint8_t len_cmd[] = {lowbyte(len), highbyte(len), command, 0x00}; + this->write_array(len_cmd, sizeof(len_cmd)); + // command value bytes if (command_value != nullptr) { - for (int i = 0; i < command_value_len; i++) { + for (uint8_t i = 0; i < command_value_len; i++) { this->write_byte(command_value[i]); } } - // footer - this->write_array(CMD_FRAME_END, 4); + // frame footer bytes + this->write_array(CMD_FRAME_FOOTER, sizeof(CMD_FRAME_FOOTER)); // FIXME to remove delay(50); // NOLINT } @@ -352,40 +437,37 @@ void LD2450Component::send_command_(uint8_t command, const uint8_t *command_valu // LD2450 Radar data message: // [AA FF 03 00] [0E 03 B1 86 10 00 40 01] [00 00 00 00 00 00 00 00] [00 00 00 00 00 00 00 00] [55 CC] // Header Target 1 Target 2 Target 3 End -void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) { - if (len < 29) { // header (4 bytes) + 8 x 3 target data + footer (2 bytes) - ESP_LOGE(TAG, "Periodic data: invalid message length"); - return; - } - if (buffer[0] != 0xAA || buffer[1] != 0xFF || buffer[2] != 0x03 || buffer[3] != 0x00) { // header - ESP_LOGE(TAG, "Periodic data: invalid message header"); - return; - } - if (buffer[len - 2] != 0x55 || buffer[len - 1] != 0xCC) { // footer - ESP_LOGE(TAG, "Periodic data: invalid message footer"); +void LD2450Component::handle_periodic_data_() { + // Early throttle check - moved before any processing to save CPU cycles + if (App.get_loop_component_start_time() - this->last_periodic_millis_ < this->throttle_) { return; } - auto current_millis = millis(); - if (current_millis - this->last_periodic_millis_ < this->throttle_) { - ESP_LOGV(TAG, "Throttling: %d", this->throttle_); + if (this->buffer_pos_ < 29) { // header (4 bytes) + 8 x 3 target data + footer (2 bytes) + ESP_LOGE(TAG, "Invalid length"); return; } - - this->last_periodic_millis_ = current_millis; + if (!ld2450::validate_header_footer(DATA_FRAME_HEADER, this->buffer_data_) || + this->buffer_data_[this->buffer_pos_ - 2] != DATA_FRAME_FOOTER[0] || + this->buffer_data_[this->buffer_pos_ - 1] != DATA_FRAME_FOOTER[1]) { + ESP_LOGE(TAG, "Invalid header/footer"); + return; + } + // Save the timestamp after validating the frame so, if invalid, we'll take the next frame immediately + this->last_periodic_millis_ = App.get_loop_component_start_time(); int16_t target_count = 0; int16_t still_target_count = 0; int16_t moving_target_count = 0; int16_t start = 0; int16_t val = 0; - uint8_t index = 0; int16_t tx = 0; int16_t ty = 0; int16_t td = 0; int16_t ts = 0; int16_t angle = 0; - std::string direction{}; + uint8_t index = 0; + Direction direction{DIRECTION_UNDEFINED}; bool is_moving = false; #if defined(USE_BINARY_SENSOR) || defined(USE_SENSOR) || defined(USE_TEXT_SENSOR) @@ -397,29 +479,38 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) { is_moving = false; sensor::Sensor *sx = this->move_x_sensors_[index]; if (sx != nullptr) { - val = ld2450::decode_coordinate(buffer[start], buffer[start + 1]); + val = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]); tx = val; - sx->publish_state(val); + if (this->cached_target_data_[index].x != val) { + sx->publish_state(val); + this->cached_target_data_[index].x = val; + } } // Y start = TARGET_Y + index * 8; sensor::Sensor *sy = this->move_y_sensors_[index]; if (sy != nullptr) { - val = ld2450::decode_coordinate(buffer[start], buffer[start + 1]); + val = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]); ty = val; - sy->publish_state(val); + if (this->cached_target_data_[index].y != val) { + sy->publish_state(val); + this->cached_target_data_[index].y = val; + } } // RESOLUTION start = TARGET_RESOLUTION + index * 8; sensor::Sensor *sr = this->move_resolution_sensors_[index]; if (sr != nullptr) { - val = (buffer[start + 1] << 8) | buffer[start]; - sr->publish_state(val); + val = (this->buffer_data_[start + 1] << 8) | this->buffer_data_[start]; + if (this->cached_target_data_[index].resolution != val) { + sr->publish_state(val); + this->cached_target_data_[index].resolution = val; + } } #endif // SPEED start = TARGET_SPEED + index * 8; - val = ld2450::decode_speed(buffer[start], buffer[start + 1]); + val = ld2450::decode_speed(this->buffer_data_[start], this->buffer_data_[start + 1]); ts = val; if (val) { is_moving = true; @@ -428,13 +519,17 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) { #ifdef USE_SENSOR sensor::Sensor *ss = this->move_speed_sensors_[index]; if (ss != nullptr) { - ss->publish_state(val); + if (this->cached_target_data_[index].speed != val) { + ss->publish_state(val); + this->cached_target_data_[index].speed = val; + } } #endif // DISTANCE - val = (uint16_t) sqrt( - pow(ld2450::decode_coordinate(buffer[TARGET_X + index * 8], buffer[(TARGET_X + index * 8) + 1]), 2) + - pow(ld2450::decode_coordinate(buffer[TARGET_Y + index * 8], buffer[(TARGET_Y + index * 8) + 1]), 2)); + // Optimized: use already decoded tx and ty values, replace pow() with multiplication + int32_t x_squared = (int32_t) tx * tx; + int32_t y_squared = (int32_t) ty * ty; + val = (uint16_t) sqrt(x_squared + y_squared); td = val; if (val > 0) { target_count++; @@ -442,27 +537,42 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) { #ifdef USE_SENSOR sensor::Sensor *sd = this->move_distance_sensors_[index]; if (sd != nullptr) { - sd->publish_state(val); + if (this->cached_target_data_[index].distance != val) { + sd->publish_state(val); + this->cached_target_data_[index].distance = val; + } } // ANGLE - angle = calculate_angle(static_cast(ty), static_cast(td)); + angle = ld2450::calculate_angle(static_cast(ty), static_cast(td)); if (tx > 0) { angle = angle * -1; } sensor::Sensor *sa = this->move_angle_sensors_[index]; if (sa != nullptr) { - sa->publish_state(angle); + if (std::isnan(this->cached_target_data_[index].angle) || + std::abs(this->cached_target_data_[index].angle - angle) > 0.1f) { + sa->publish_state(angle); + this->cached_target_data_[index].angle = angle; + } } #endif #ifdef USE_TEXT_SENSOR // DIRECTION - direction = get_direction(ts); if (td == 0) { - direction = "NA"; + direction = DIRECTION_NA; + } else if (ts > 0) { + direction = DIRECTION_MOVING_AWAY; + } else if (ts < 0) { + direction = DIRECTION_APPROACHING; + } else { + direction = DIRECTION_STATIONARY; } text_sensor::TextSensor *tsd = this->direction_text_sensors_[index]; if (tsd != nullptr) { - tsd->publish_state(direction); + if (this->cached_target_data_[index].direction != direction) { + tsd->publish_state(find_str(ld2450::DIRECTION_BY_UINT, direction)); + this->cached_target_data_[index].direction = direction; + } } #endif @@ -489,32 +599,50 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) { // Publish Still Target Count in Zones sensor::Sensor *szstc = this->zone_still_target_count_sensors_[index]; if (szstc != nullptr) { - szstc->publish_state(zone_still_targets); + if (this->cached_zone_data_[index].still_count != zone_still_targets) { + szstc->publish_state(zone_still_targets); + this->cached_zone_data_[index].still_count = zone_still_targets; + } } // Publish Moving Target Count in Zones sensor::Sensor *szmtc = this->zone_moving_target_count_sensors_[index]; if (szmtc != nullptr) { - szmtc->publish_state(zone_moving_targets); + if (this->cached_zone_data_[index].moving_count != zone_moving_targets) { + szmtc->publish_state(zone_moving_targets); + this->cached_zone_data_[index].moving_count = zone_moving_targets; + } } // Publish All Target Count in Zones sensor::Sensor *sztc = this->zone_target_count_sensors_[index]; if (sztc != nullptr) { - sztc->publish_state(zone_all_targets); + if (this->cached_zone_data_[index].total_count != zone_all_targets) { + sztc->publish_state(zone_all_targets); + this->cached_zone_data_[index].total_count = zone_all_targets; + } } } // End loop thru zones // Target Count if (this->target_count_sensor_ != nullptr) { - this->target_count_sensor_->publish_state(target_count); + if (this->cached_global_data_.target_count != target_count) { + this->target_count_sensor_->publish_state(target_count); + this->cached_global_data_.target_count = target_count; + } } // Still Target Count if (this->still_target_count_sensor_ != nullptr) { - this->still_target_count_sensor_->publish_state(still_target_count); + if (this->cached_global_data_.still_count != still_target_count) { + this->still_target_count_sensor_->publish_state(still_target_count); + this->cached_global_data_.still_count = still_target_count; + } } // Moving Target Count if (this->moving_target_count_sensor_ != nullptr) { - this->moving_target_count_sensor_->publish_state(moving_target_count); + if (this->cached_global_data_.moving_count != moving_target_count) { + this->moving_target_count_sensor_->publish_state(moving_target_count); + this->cached_global_data_.moving_count = moving_target_count; + } } #endif @@ -555,128 +683,150 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) { #ifdef USE_SENSOR // For presence timeout check if (target_count > 0) { - this->presence_millis_ = millis(); + this->presence_millis_ = App.get_loop_component_start_time(); } if (moving_target_count > 0) { - this->moving_presence_millis_ = millis(); + this->moving_presence_millis_ = App.get_loop_component_start_time(); } if (still_target_count > 0) { - this->still_presence_millis_ = millis(); + this->still_presence_millis_ = App.get_loop_component_start_time(); } #endif } -bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) { - ESP_LOGV(TAG, "Handling ack data for command %02X", buffer[COMMAND]); - if (len < 10) { - ESP_LOGE(TAG, "Ack data: invalid length"); +bool LD2450Component::handle_ack_data_() { + ESP_LOGV(TAG, "Handling ACK DATA for COMMAND %02X", this->buffer_data_[COMMAND]); + if (this->buffer_pos_ < 10) { + ESP_LOGE(TAG, "Invalid length"); return true; } - if (buffer[0] != 0xFD || buffer[1] != 0xFC || buffer[2] != 0xFB || buffer[3] != 0xFA) { // frame header - ESP_LOGE(TAG, "Ack data: invalid header (command %02X)", buffer[COMMAND]); + if (!ld2450::validate_header_footer(CMD_FRAME_HEADER, this->buffer_data_)) { + ESP_LOGW(TAG, "Invalid header: %s", format_hex_pretty(this->buffer_data_, HEADER_FOOTER_SIZE).c_str()); return true; } - if (buffer[COMMAND_STATUS] != 0x01) { - ESP_LOGE(TAG, "Ack data: invalid status"); + if (this->buffer_data_[COMMAND_STATUS] != 0x01) { + ESP_LOGE(TAG, "Invalid status"); return true; } - if (buffer[8] || buffer[9]) { - ESP_LOGE(TAG, "Ack data: last buffer was %u, %u", buffer[8], buffer[9]); + if (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; } - switch (buffer[COMMAND]) { - case lowbyte(CMD_ENABLE_CONF): - ESP_LOGV(TAG, "Got enable conf command"); + switch (this->buffer_data_[COMMAND]) { + case CMD_ENABLE_CONF: + ESP_LOGV(TAG, "Enable conf"); break; - case lowbyte(CMD_DISABLE_CONF): - ESP_LOGV(TAG, "Got disable conf command"); + + case CMD_DISABLE_CONF: + ESP_LOGV(TAG, "Disabled conf"); break; - case lowbyte(CMD_SET_BAUD_RATE): - ESP_LOGV(TAG, "Got baud rate change command"); + + case CMD_SET_BAUD_RATE: + ESP_LOGV(TAG, "Baud rate change"); #ifdef USE_SELECT if (this->baud_rate_select_ != nullptr) { - ESP_LOGV(TAG, "Change baud rate to %s", this->baud_rate_select_->state.c_str()); + ESP_LOGE(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->state.c_str()); } #endif break; - case lowbyte(CMD_VERSION): - this->version_ = ld2450::format_version(buffer); - ESP_LOGV(TAG, "Firmware version: %s", this->version_.c_str()); + + case CMD_QUERY_VERSION: { + std::memcpy(this->version_, &this->buffer_data_[12], sizeof(this->version_)); + std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5], + this->version_[4], this->version_[3], this->version_[2]); + ESP_LOGV(TAG, "Firmware version: %s", version.c_str()); #ifdef USE_TEXT_SENSOR if (this->version_text_sensor_ != nullptr) { - this->version_text_sensor_->publish_state(this->version_); + this->version_text_sensor_->publish_state(version); } #endif break; - case lowbyte(CMD_MAC): - if (len < 20) { + } + + case CMD_QUERY_MAC_ADDRESS: { + if (this->buffer_pos_ < 20) { return false; } - this->mac_ = ld2450::format_mac(buffer); - ESP_LOGV(TAG, "MAC address: %s", this->mac_.c_str()); + + this->bluetooth_on_ = std::memcmp(&this->buffer_data_[10], NO_MAC, sizeof(NO_MAC)) != 0; + if (this->bluetooth_on_) { + std::memcpy(this->mac_address_, &this->buffer_data_[10], sizeof(this->mac_address_)); + } + + std::string mac_str = + mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC; + ESP_LOGV(TAG, "MAC address: %s", mac_str.c_str()); #ifdef USE_TEXT_SENSOR if (this->mac_text_sensor_ != nullptr) { - this->mac_text_sensor_->publish_state(this->mac_ == NO_MAC ? UNKNOWN_MAC : this->mac_); + this->mac_text_sensor_->publish_state(mac_str); } #endif #ifdef USE_SWITCH if (this->bluetooth_switch_ != nullptr) { - this->bluetooth_switch_->publish_state(this->mac_ != NO_MAC); + this->bluetooth_switch_->publish_state(this->bluetooth_on_); } #endif break; - case lowbyte(CMD_BLUETOOTH): - ESP_LOGV(TAG, "Got Bluetooth command"); + } + + case CMD_BLUETOOTH: + ESP_LOGV(TAG, "Bluetooth"); break; - case lowbyte(CMD_SINGLE_TARGET_MODE): - ESP_LOGV(TAG, "Got single target conf command"); + + case CMD_SINGLE_TARGET_MODE: + ESP_LOGV(TAG, "Single target conf"); #ifdef USE_SWITCH if (this->multi_target_switch_ != nullptr) { this->multi_target_switch_->publish_state(false); } #endif break; - case lowbyte(CMD_MULTI_TARGET_MODE): - ESP_LOGV(TAG, "Got multi target conf command"); + + case CMD_MULTI_TARGET_MODE: + ESP_LOGV(TAG, "Multi target conf"); #ifdef USE_SWITCH if (this->multi_target_switch_ != nullptr) { this->multi_target_switch_->publish_state(true); } #endif break; - case lowbyte(CMD_QUERY_TARGET_MODE): - ESP_LOGV(TAG, "Got query target tracking mode command"); + + case CMD_QUERY_TARGET_MODE: + ESP_LOGV(TAG, "Query target tracking mode"); #ifdef USE_SWITCH if (this->multi_target_switch_ != nullptr) { - this->multi_target_switch_->publish_state(buffer[10] == 0x02); + this->multi_target_switch_->publish_state(this->buffer_data_[10] == 0x02); } #endif break; - case lowbyte(CMD_QUERY_ZONE): - ESP_LOGV(TAG, "Got query zone conf command"); - this->zone_type_ = std::stoi(std::to_string(buffer[10]), nullptr, 16); + + case CMD_QUERY_ZONE: + ESP_LOGV(TAG, "Query zone conf"); + this->zone_type_ = std::stoi(std::to_string(this->buffer_data_[10]), nullptr, 16); this->publish_zone_type(); #ifdef USE_SELECT if (this->zone_type_select_ != nullptr) { ESP_LOGV(TAG, "Change zone type to: %s", this->zone_type_select_->state.c_str()); } #endif - if (buffer[10] == 0x00) { + if (this->buffer_data_[10] == 0x00) { ESP_LOGV(TAG, "Zone: Disabled"); } - if (buffer[10] == 0x01) { + if (this->buffer_data_[10] == 0x01) { ESP_LOGV(TAG, "Zone: Area detection"); } - if (buffer[10] == 0x02) { + if (this->buffer_data_[10] == 0x02) { ESP_LOGV(TAG, "Zone: Area filter"); } - this->process_zone_(buffer); + this->process_zone_(); break; - case lowbyte(CMD_SET_ZONE): - ESP_LOGV(TAG, "Got set zone conf command"); + + case CMD_SET_ZONE: + ESP_LOGV(TAG, "Set zone conf"); this->query_zone_info(); break; + default: break; } @@ -684,62 +834,64 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) { } // Read LD2450 buffer data -void LD2450Component::readline_(int readch, uint8_t *buffer, uint8_t len) { +void LD2450Component::readline_(int readch) { if (readch < 0) { - return; + return; // No data available } - if (this->buffer_pos_ < len - 1) { - buffer[this->buffer_pos_++] = readch; - buffer[this->buffer_pos_] = 0; + + if (this->buffer_pos_ < MAX_LINE_LENGTH - 1) { + this->buffer_data_[this->buffer_pos_++] = readch; + this->buffer_data_[this->buffer_pos_] = 0; } else { + // We should never get here, but just in case... + ESP_LOGW(TAG, "Max command length exceeded; ignoring"); this->buffer_pos_ = 0; } if (this->buffer_pos_ < 4) { - return; + return; // Not enough data to process yet } - if (buffer[this->buffer_pos_ - 2] == 0x55 && buffer[this->buffer_pos_ - 1] == 0xCC) { - ESP_LOGV(TAG, "Handle periodic radar data"); - this->handle_periodic_data_(buffer, this->buffer_pos_); + if (this->buffer_data_[this->buffer_pos_ - 2] == DATA_FRAME_FOOTER[0] && + this->buffer_data_[this->buffer_pos_ - 1] == DATA_FRAME_FOOTER[1]) { + ESP_LOGV(TAG, "Handling Periodic Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str()); + this->handle_periodic_data_(); this->buffer_pos_ = 0; // Reset position index for next frame - } else if (buffer[this->buffer_pos_ - 4] == 0x04 && buffer[this->buffer_pos_ - 3] == 0x03 && - buffer[this->buffer_pos_ - 2] == 0x02 && buffer[this->buffer_pos_ - 1] == 0x01) { - ESP_LOGV(TAG, "Handle command ack data"); - if (this->handle_ack_data_(buffer, this->buffer_pos_)) { - this->buffer_pos_ = 0; // Reset position index for next frame + } else if (ld2450::validate_header_footer(CMD_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) { + ESP_LOGV(TAG, "Handling Ack Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str()); + if (this->handle_ack_data_()) { + this->buffer_pos_ = 0; // Reset position index for next message } else { - ESP_LOGV(TAG, "Command ack data invalid"); + ESP_LOGV(TAG, "Ack Data incomplete"); } } } // Set Config Mode - Pre-requisite sending commands void LD2450Component::set_config_mode_(bool enable) { - uint8_t cmd = enable ? CMD_ENABLE_CONF : CMD_DISABLE_CONF; - uint8_t cmd_value[2] = {0x01, 0x00}; - this->send_command_(cmd, enable ? cmd_value : nullptr, 2); + const uint8_t cmd = enable ? CMD_ENABLE_CONF : CMD_DISABLE_CONF; + const uint8_t cmd_value[2] = {0x01, 0x00}; + this->send_command_(cmd, enable ? cmd_value : nullptr, sizeof(cmd_value)); } // Set Bluetooth Enable/Disable void LD2450Component::set_bluetooth(bool enable) { this->set_config_mode_(true); - uint8_t enable_cmd_value[2] = {0x01, 0x00}; - uint8_t disable_cmd_value[2] = {0x00, 0x00}; - this->send_command_(CMD_BLUETOOTH, enable ? enable_cmd_value : disable_cmd_value, 2); + const uint8_t cmd_value[2] = {enable ? (uint8_t) 0x01 : (uint8_t) 0x00, 0x00}; + this->send_command_(CMD_BLUETOOTH, cmd_value, sizeof(cmd_value)); this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); } // Set Baud rate void LD2450Component::set_baud_rate(const std::string &state) { this->set_config_mode_(true); - uint8_t cmd_value[2] = {BAUD_RATE_ENUM_TO_INT.at(state), 0x00}; - this->send_command_(CMD_SET_BAUD_RATE, cmd_value, 2); + const uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00}; + this->send_command_(CMD_SET_BAUD_RATE, cmd_value, sizeof(cmd_value)); this->set_timeout(200, [this]() { this->restart_(); }); } // Set Zone Type - one of: Disabled, Detection, Filter void LD2450Component::set_zone_type(const std::string &state) { ESP_LOGV(TAG, "Set zone type: %s", state.c_str()); - uint8_t zone_type = ZONE_TYPE_ENUM_TO_INT.at(state); + uint8_t zone_type = find_uint8(ZONE_TYPE_BY_STR, state); this->zone_type_ = zone_type; this->send_set_zone_command_(); } @@ -747,7 +899,7 @@ void LD2450Component::set_zone_type(const std::string &state) { // Publish Zone Type to Select component void LD2450Component::publish_zone_type() { #ifdef USE_SELECT - std::string zone_type = ZONE_TYPE_INT_TO_ENUM.at(static_cast(this->zone_type_)); + std::string zone_type = find_str(ZONE_TYPE_BY_UINT, this->zone_type_); if (this->zone_type_select_ != nullptr) { this->zone_type_select_->publish_state(zone_type); } @@ -773,12 +925,12 @@ void LD2450Component::factory_reset() { void LD2450Component::restart_() { this->send_command_(CMD_RESTART, nullptr, 0); } // Get LD2450 firmware version -void LD2450Component::get_version_() { this->send_command_(CMD_VERSION, nullptr, 0); } +void LD2450Component::get_version_() { this->send_command_(CMD_QUERY_VERSION, nullptr, 0); } // Get LD2450 mac address void LD2450Component::get_mac_() { uint8_t cmd_value[2] = {0x01, 0x00}; - this->send_command_(CMD_MAC, cmd_value, 2); + this->send_command_(CMD_QUERY_MAC_ADDRESS, cmd_value, 2); } // Query for target tracking mode diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h index e0927e5d7d..ae72a0d8cb 100644 --- a/esphome/components/ld2450/ld2450.h +++ b/esphome/components/ld2450/ld2450.h @@ -1,12 +1,12 @@ #pragma once -#include -#include #include "esphome/components/uart/uart.h" #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include "esphome/core/preferences.h" +#include +#include #ifdef USE_SENSOR #include "esphome/components/sensor/sensor.h" #endif @@ -38,10 +38,18 @@ namespace ld2450 { // Constants static const uint8_t DEFAULT_PRESENCE_TIMEOUT = 5; // Timeout to reset presense status 5 sec. -static const uint8_t MAX_LINE_LENGTH = 60; // Max characters for serial buffer +static const uint8_t MAX_LINE_LENGTH = 41; // Max characters for serial buffer static const uint8_t MAX_TARGETS = 3; // Max 3 Targets in LD2450 static const uint8_t MAX_ZONES = 3; // Max 3 Zones in LD2450 +enum Direction : uint8_t { + DIRECTION_APPROACHING = 0, + DIRECTION_MOVING_AWAY = 1, + DIRECTION_STATIONARY = 2, + DIRECTION_NA = 3, + DIRECTION_UNDEFINED = 4, +}; + // Target coordinate struct struct Target { int16_t x; @@ -66,63 +74,23 @@ struct ZoneOfNumbers { }; #endif -enum BaudRateStructure : uint8_t { - BAUD_RATE_9600 = 1, - BAUD_RATE_19200 = 2, - BAUD_RATE_38400 = 3, - BAUD_RATE_57600 = 4, - BAUD_RATE_115200 = 5, - BAUD_RATE_230400 = 6, - BAUD_RATE_256000 = 7, - BAUD_RATE_460800 = 8 -}; - -// Convert baud rate enum to int -static const std::map BAUD_RATE_ENUM_TO_INT{ - {"9600", BAUD_RATE_9600}, {"19200", BAUD_RATE_19200}, {"38400", BAUD_RATE_38400}, - {"57600", BAUD_RATE_57600}, {"115200", BAUD_RATE_115200}, {"230400", BAUD_RATE_230400}, - {"256000", BAUD_RATE_256000}, {"460800", BAUD_RATE_460800}}; - -// Zone type struct -enum ZoneTypeStructure : uint8_t { ZONE_DISABLED = 0, ZONE_DETECTION = 1, ZONE_FILTER = 2 }; - -// Convert zone type int to enum -static const std::map ZONE_TYPE_INT_TO_ENUM{ - {ZONE_DISABLED, "Disabled"}, {ZONE_DETECTION, "Detection"}, {ZONE_FILTER, "Filter"}}; - -// Convert zone type enum to int -static const std::map ZONE_TYPE_ENUM_TO_INT{ - {"Disabled", ZONE_DISABLED}, {"Detection", ZONE_DETECTION}, {"Filter", ZONE_FILTER}}; - -// LD2450 serial command header & footer -static const uint8_t CMD_FRAME_HEADER[4] = {0xFD, 0xFC, 0xFB, 0xFA}; -static const uint8_t CMD_FRAME_END[4] = {0x04, 0x03, 0x02, 0x01}; - -enum PeriodicDataStructure : uint8_t { - TARGET_X = 4, - TARGET_Y = 6, - TARGET_SPEED = 8, - TARGET_RESOLUTION = 10, -}; - -enum PeriodicDataValue : uint8_t { HEAD = 0xAA, END = 0x55, CHECK = 0x00 }; - -enum AckDataStructure : uint8_t { COMMAND = 6, COMMAND_STATUS = 7 }; - class LD2450Component : public Component, public uart::UARTDevice { -#ifdef USE_SENSOR - SUB_SENSOR(target_count) - SUB_SENSOR(still_target_count) - SUB_SENSOR(moving_target_count) -#endif #ifdef USE_BINARY_SENSOR - SUB_BINARY_SENSOR(target) SUB_BINARY_SENSOR(moving_target) SUB_BINARY_SENSOR(still_target) + SUB_BINARY_SENSOR(target) +#endif +#ifdef USE_SENSOR + SUB_SENSOR(moving_target_count) + SUB_SENSOR(still_target_count) + SUB_SENSOR(target_count) #endif #ifdef USE_TEXT_SENSOR - SUB_TEXT_SENSOR(version) SUB_TEXT_SENSOR(mac) + SUB_TEXT_SENSOR(version) +#endif +#ifdef USE_NUMBER + SUB_NUMBER(presence_timeout) #endif #ifdef USE_SELECT SUB_SELECT(baud_rate) @@ -133,20 +101,16 @@ class LD2450Component : public Component, public uart::UARTDevice { SUB_SWITCH(multi_target) #endif #ifdef USE_BUTTON - SUB_BUTTON(reset) + SUB_BUTTON(factory_reset) SUB_BUTTON(restart) #endif -#ifdef USE_NUMBER - SUB_NUMBER(presence_timeout) -#endif public: - LD2450Component(); void setup() override; void dump_config() override; void loop() override; void set_presence_timeout(); - void set_throttle(uint16_t value) { this->throttle_ = value; }; + void set_throttle(uint16_t value) { this->throttle_ = value; } void read_all_info(); void query_zone_info(); void restart_and_read_all_info(); @@ -182,10 +146,10 @@ class LD2450Component : public Component, public uart::UARTDevice { protected: void send_command_(uint8_t command_str, const uint8_t *command_value, uint8_t command_value_len); void set_config_mode_(bool enable); - void handle_periodic_data_(uint8_t *buffer, uint8_t len); - bool handle_ack_data_(uint8_t *buffer, uint8_t len); - void process_zone_(uint8_t *buffer); - void readline_(int readch, uint8_t *buffer, uint8_t len); + void handle_periodic_data_(); + bool handle_ack_data_(); + void process_zone_(); + void readline_(int readch); void get_version_(); void get_mac_(); void query_target_tracking_mode_(); @@ -197,19 +161,46 @@ class LD2450Component : public Component, public uart::UARTDevice { bool get_timeout_status_(uint32_t check_millis); uint8_t count_targets_in_zone_(const Zone &zone, bool is_moving); - Target target_info_[MAX_TARGETS]; - Zone zone_config_[MAX_ZONES]; - uint8_t buffer_pos_ = 0; // where to resume processing/populating buffer - uint8_t buffer_data_[MAX_LINE_LENGTH]; uint32_t last_periodic_millis_ = 0; uint32_t presence_millis_ = 0; uint32_t still_presence_millis_ = 0; uint32_t moving_presence_millis_ = 0; uint16_t throttle_ = 0; uint16_t timeout_ = 5; + uint8_t buffer_data_[MAX_LINE_LENGTH]; + uint8_t mac_address_[6] = {0, 0, 0, 0, 0, 0}; + uint8_t version_[6] = {0, 0, 0, 0, 0, 0}; + uint8_t buffer_pos_ = 0; // where to resume processing/populating buffer uint8_t zone_type_ = 0; - std::string version_{}; - std::string mac_{}; + bool bluetooth_on_{false}; + Target target_info_[MAX_TARGETS]; + Zone zone_config_[MAX_ZONES]; + + // Change detection - cache previous values to avoid redundant publishes + // All values are initialized to sentinel values that are outside the valid sensor ranges + // to ensure the first real measurement is always published + struct CachedTargetData { + int16_t x = std::numeric_limits::min(); // -32768, outside range of -4860 to 4860 + int16_t y = std::numeric_limits::min(); // -32768, outside range of 0 to 7560 + int16_t speed = std::numeric_limits::min(); // -32768, outside practical sensor range + uint16_t resolution = std::numeric_limits::max(); // 65535, unlikely resolution value + uint16_t distance = std::numeric_limits::max(); // 65535, outside range of 0 to ~8990 + Direction direction = DIRECTION_UNDEFINED; // Undefined, will differ from any real direction + float angle = NAN; // NAN, safe sentinel for floats + } cached_target_data_[MAX_TARGETS]; + + struct CachedZoneData { + uint8_t still_count = std::numeric_limits::max(); // 255, unlikely zone count + uint8_t moving_count = std::numeric_limits::max(); // 255, unlikely zone count + uint8_t total_count = std::numeric_limits::max(); // 255, unlikely zone count + } cached_zone_data_[MAX_ZONES]; + + struct CachedGlobalData { + uint8_t target_count = std::numeric_limits::max(); // 255, max 3 targets possible + uint8_t still_count = std::numeric_limits::max(); // 255, max 3 targets possible + uint8_t moving_count = std::numeric_limits::max(); // 255, max 3 targets possible + } cached_global_data_; + #ifdef USE_NUMBER ESPPreferenceObject pref_; // only used when numbers are in use ZoneOfNumbers zone_numbers_[MAX_ZONES]; diff --git a/esphome/components/ld2450/switch/__init__.py b/esphome/components/ld2450/switch/__init__.py index fb3969cf50..2d76b75781 100644 --- a/esphome/components/ld2450/switch/__init__.py +++ b/esphome/components/ld2450/switch/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg from esphome.components import switch import esphome.config_validation as cv from esphome.const import ( + CONF_BLUETOOTH, DEVICE_CLASS_SWITCH, ENTITY_CATEGORY_CONFIG, ICON_BLUETOOTH, @@ -13,7 +14,6 @@ from .. import CONF_LD2450_ID, LD2450Component, ld2450_ns BluetoothSwitch = ld2450_ns.class_("BluetoothSwitch", switch.Switch) MultiTargetSwitch = ld2450_ns.class_("MultiTargetSwitch", switch.Switch) -CONF_BLUETOOTH = "bluetooth" CONF_MULTI_TARGET = "multi_target" CONFIG_SCHEMA = { diff --git a/esphome/components/ledc/ledc_output.cpp b/esphome/components/ledc/ledc_output.cpp index aefe0e63d8..2ae2656f54 100644 --- a/esphome/components/ledc/ledc_output.cpp +++ b/esphome/components/ledc/ledc_output.cpp @@ -3,28 +3,16 @@ #ifdef USE_ESP32 -#ifdef USE_ARDUINO -#include -#endif #include - #include #define CLOCK_FREQUENCY 80e6f -#ifdef USE_ARDUINO -#ifdef SOC_LEDC_SUPPORT_XTAL_CLOCK -#undef CLOCK_FREQUENCY -// starting with ESP32 Arduino 2.0.2, the 40MHz crystal is used as clock by default if supported -#define CLOCK_FREQUENCY 40e6f -#endif -#else #ifdef SOC_LEDC_SUPPORT_APB_CLOCK #define DEFAULT_CLK LEDC_USE_APB_CLK #else #define DEFAULT_CLK LEDC_AUTO_CLK #endif -#endif static const uint8_t SETUP_ATTEMPT_COUNT_MAX = 5; @@ -34,7 +22,6 @@ namespace ledc { static const char *const TAG = "ledc.output"; static const int MAX_RES_BITS = LEDC_TIMER_BIT_MAX - 1; -#ifdef USE_ESP_IDF #if SOC_LEDC_SUPPORT_HS_MODE // Only ESP32 has LEDC_HIGH_SPEED_MODE inline ledc_mode_t get_speed_mode(uint8_t channel) { return channel < 8 ? LEDC_HIGH_SPEED_MODE : LEDC_LOW_SPEED_MODE; } @@ -44,7 +31,6 @@ inline ledc_mode_t get_speed_mode(uint8_t channel) { return channel < 8 ? LEDC_H // https://docs.espressif.com/projects/esp-idf/en/latest/esp32c3/api-reference/peripherals/ledc.html#functionality-overview inline ledc_mode_t get_speed_mode(uint8_t) { return LEDC_LOW_SPEED_MODE; } #endif -#endif float ledc_max_frequency_for_bit_depth(uint8_t bit_depth) { return static_cast(CLOCK_FREQUENCY) / static_cast(1 << bit_depth); @@ -68,7 +54,6 @@ optional ledc_bit_depth_for_frequency(float frequency) { return {}; } -#ifdef USE_ESP_IDF esp_err_t configure_timer_frequency(ledc_mode_t speed_mode, ledc_timer_t timer_num, ledc_channel_t chan_num, uint8_t channel, uint8_t &bit_depth, float frequency) { bit_depth = *ledc_bit_depth_for_frequency(frequency); @@ -98,13 +83,10 @@ esp_err_t configure_timer_frequency(ledc_mode_t speed_mode, ledc_timer_t timer_n return init_result; } -#endif -#ifdef USE_ESP_IDF constexpr int ledc_angle_to_htop(float angle, uint8_t bit_depth) { return static_cast(angle * ((1U << bit_depth) - 1) / 360.0f); } -#endif // USE_ESP_IDF void LEDCOutput::write_state(float state) { if (!this->initialized_) { @@ -120,10 +102,6 @@ void LEDCOutput::write_state(float state) { const float duty_rounded = roundf(state * max_duty); auto duty = static_cast(duty_rounded); ESP_LOGV(TAG, "Setting duty: %" PRIu32 " on channel %u", duty, this->channel_); -#ifdef USE_ARDUINO - ledcWrite(this->channel_, duty); -#endif -#ifdef USE_ESP_IDF auto speed_mode = get_speed_mode(this->channel_); auto chan_num = static_cast(this->channel_ % 8); int hpoint = ledc_angle_to_htop(this->phase_angle_, this->bit_depth_); @@ -135,18 +113,10 @@ void LEDCOutput::write_state(float state) { ledc_set_duty_with_hpoint(speed_mode, chan_num, duty, hpoint); ledc_update_duty(speed_mode, chan_num); } -#endif } void LEDCOutput::setup() { ESP_LOGCONFIG(TAG, "Running setup"); -#ifdef USE_ARDUINO - this->update_frequency(this->frequency_); - this->turn_off(); - // Attach pin after setting default value - ledcAttachPin(this->pin_->get_pin(), this->channel_); -#endif -#ifdef USE_ESP_IDF auto speed_mode = get_speed_mode(this->channel_); auto timer_num = static_cast((this->channel_ % 8) / 2); auto chan_num = static_cast(this->channel_ % 8); @@ -175,7 +145,6 @@ void LEDCOutput::setup() { ledc_channel_config(&chan_conf); this->initialized_ = true; this->status_clear_error(); -#endif } void LEDCOutput::dump_config() { @@ -208,38 +177,7 @@ void LEDCOutput::update_frequency(float frequency) { } this->bit_depth_ = bit_depth_opt.value_or(8); this->frequency_ = frequency; -#ifdef USE_ARDUINO - ESP_LOGV(TAG, "Using Arduino API - Trying to define channel, frequency and bit depth"); - u_int32_t configured_frequency = 0; - // Configure LEDC channel, frequency and bit depth with fallback - int attempt_count_max = SETUP_ATTEMPT_COUNT_MAX; - while (attempt_count_max > 0 && configured_frequency == 0) { - ESP_LOGV(TAG, "Initializing channel %u with frequency %.1f and bit depth of %u", this->channel_, this->frequency_, - this->bit_depth_); - configured_frequency = ledcSetup(this->channel_, frequency, this->bit_depth_); - if (configured_frequency != 0) { - this->initialized_ = true; - this->status_clear_error(); - ESP_LOGV(TAG, "Configured frequency: %u with bit depth: %u", configured_frequency, this->bit_depth_); - } else { - ESP_LOGW(TAG, "Unable to initialize channel %u with frequency %.1f and bit depth of %u", this->channel_, - this->frequency_, this->bit_depth_); - // try again with a lower bit depth - this->bit_depth_--; - } - attempt_count_max--; - } - - if (configured_frequency == 0) { - ESP_LOGE(TAG, "Permanently failed to initialize channel %u with frequency %.1f and bit depth of %u", this->channel_, - this->frequency_, this->bit_depth_); - this->status_set_error(); - return; - } - -#endif // USE_ARDUINO -#ifdef USE_ESP_IDF if (!this->initialized_) { ESP_LOGW(TAG, "Not yet initialized"); return; @@ -259,7 +197,7 @@ void LEDCOutput::update_frequency(float frequency) { } this->status_clear_error(); -#endif + // re-apply duty this->write_state(this->duty_); } diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index 5bdfb15e19..149e5d1179 100644 --- a/esphome/components/libretiny/__init__.py +++ b/esphome/components/libretiny/__init__.py @@ -173,9 +173,9 @@ def _notify_old_style(config): # The dev and latest branches will be at *least* this version, which is what matters. ARDUINO_VERSIONS = { - "dev": (cv.Version(1, 7, 0), "https://github.com/libretiny-eu/libretiny.git"), - "latest": (cv.Version(1, 7, 0), "libretiny"), - "recommended": (cv.Version(1, 7, 0), None), + "dev": (cv.Version(1, 9, 1), "https://github.com/libretiny-eu/libretiny.git"), + "latest": (cv.Version(1, 9, 1), "libretiny"), + "recommended": (cv.Version(1, 9, 1), None), } @@ -264,6 +264,7 @@ async def component_to_code(config): # force using arduino framework cg.add_platformio_option("framework", "arduino") cg.add_build_flag("-DUSE_ARDUINO") + cg.set_cpp_standard("gnu++20") # disable library compatibility checks cg.add_platformio_option("lib_ldf_mode", "off") diff --git a/esphome/components/libretiny/const.py b/esphome/components/libretiny/const.py index 362609df44..671992f8bd 100644 --- a/esphome/components/libretiny/const.py +++ b/esphome/components/libretiny/const.py @@ -50,6 +50,7 @@ KEY_FAMILY = "family" # COMPONENTS - auto-generated! Do not modify this block. COMPONENT_BK72XX = "bk72xx" +COMPONENT_LN882X = "ln882x" COMPONENT_RTL87XX = "rtl87xx" # COMPONENTS - end @@ -58,6 +59,7 @@ FAMILY_BK7231N = "BK7231N" FAMILY_BK7231Q = "BK7231Q" FAMILY_BK7231T = "BK7231T" FAMILY_BK7251 = "BK7251" +FAMILY_LN882H = "LN882H" FAMILY_RTL8710B = "RTL8710B" FAMILY_RTL8720C = "RTL8720C" FAMILIES = [ @@ -65,6 +67,7 @@ FAMILIES = [ FAMILY_BK7231Q, FAMILY_BK7231T, FAMILY_BK7251, + FAMILY_LN882H, FAMILY_RTL8710B, FAMILY_RTL8720C, ] @@ -73,6 +76,7 @@ FAMILY_FRIENDLY = { FAMILY_BK7231Q: "BK7231Q", FAMILY_BK7231T: "BK7231T", FAMILY_BK7251: "BK7251", + FAMILY_LN882H: "LN882H", FAMILY_RTL8710B: "RTL8710B", FAMILY_RTL8720C: "RTL8720C", } @@ -81,6 +85,7 @@ FAMILY_COMPONENT = { FAMILY_BK7231Q: COMPONENT_BK72XX, FAMILY_BK7231T: COMPONENT_BK72XX, FAMILY_BK7251: COMPONENT_BK72XX, + FAMILY_LN882H: COMPONENT_LN882X, FAMILY_RTL8710B: COMPONENT_RTL87XX, FAMILY_RTL8720C: COMPONENT_RTL87XX, } diff --git a/esphome/components/libretiny/generate_components.py b/esphome/components/libretiny/generate_components.py index ae55fd9e40..c750b79317 100644 --- a/esphome/components/libretiny/generate_components.py +++ b/esphome/components/libretiny/generate_components.py @@ -94,6 +94,7 @@ PIN_SCHEMA_EXTRA = f"libretiny.BASE_PIN_SCHEMA.extend({VAR_PIN_SCHEMA})" COMPONENT_MAP = { "rtl87xx": "realtek-amb", "bk72xx": "beken-72xx", + "ln882x": "lightning-ln882x", } diff --git a/esphome/components/libretiny/helpers.cpp b/esphome/components/libretiny/helpers.cpp new file mode 100644 index 0000000000..b6451860d5 --- /dev/null +++ b/esphome/components/libretiny/helpers.cpp @@ -0,0 +1,35 @@ +#include "esphome/core/helpers.h" + +#ifdef USE_LIBRETINY + +#include "esphome/core/hal.h" + +#include // for macAddress() + +namespace esphome { + +uint32_t random_uint32() { return rand(); } + +bool random_bytes(uint8_t *data, size_t len) { + lt_rand_bytes(data, len); + return true; +} + +Mutex::Mutex() { handle_ = xSemaphoreCreateMutex(); } +Mutex::~Mutex() {} +void Mutex::lock() { xSemaphoreTake(this->handle_, portMAX_DELAY); } +bool Mutex::try_lock() { return xSemaphoreTake(this->handle_, 0) == pdTRUE; } +void Mutex::unlock() { xSemaphoreGive(this->handle_); } + +// only affects the executing core +// so should not be used as a mutex lock, only to get accurate timing +IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); } +IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); } + +void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter) + WiFi.macAddress(mac); +} + +} // namespace esphome + +#endif // USE_LIBRETINY diff --git a/esphome/components/libretiny/lt_component.cpp b/esphome/components/libretiny/lt_component.cpp index ec4b60eaeb..ffccd0ad7a 100644 --- a/esphome/components/libretiny/lt_component.cpp +++ b/esphome/components/libretiny/lt_component.cpp @@ -10,9 +10,11 @@ namespace libretiny { static const char *const TAG = "lt.component"; void LTComponent::dump_config() { - ESP_LOGCONFIG(TAG, "LibreTiny:"); - ESP_LOGCONFIG(TAG, " Version: %s", LT_BANNER_STR + 10); - ESP_LOGCONFIG(TAG, " Loglevel: %u", LT_LOGLEVEL); + ESP_LOGCONFIG(TAG, + "LibreTiny:\n" + " Version: %s\n" + " Loglevel: %u", + LT_BANNER_STR + 10, LT_LOGLEVEL); #ifdef USE_TEXT_SENSOR if (this->version_ != nullptr) { diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py index a013029fc2..7ab899edb2 100644 --- a/esphome/components/light/__init__.py +++ b/esphome/components/light/__init__.py @@ -38,8 +38,8 @@ from esphome.const import ( CONF_WHITE, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity from .automation import LIGHT_STATE_SCHEMA from .effects import ( @@ -110,6 +110,8 @@ LIGHT_SCHEMA = ( ) ) +LIGHT_SCHEMA.add_extra(entity_duplicate_validator("light")) + BINARY_LIGHT_SCHEMA = LIGHT_SCHEMA.extend( { cv.Optional(CONF_EFFECTS): validate_effects(BINARY_EFFECTS), @@ -207,7 +209,7 @@ def validate_color_temperature_channels(value): async def setup_light_core_(light_var, output_var, config): - await setup_entity(light_var, config) + await setup_entity(light_var, config, "light") cg.add(light_var.set_restore_mode(config[CONF_RESTORE_MODE])) diff --git a/esphome/components/light/addressable_light.h b/esphome/components/light/addressable_light.h index 8302239d6a..baa4507d2f 100644 --- a/esphome/components/light/addressable_light.h +++ b/esphome/components/light/addressable_light.h @@ -97,12 +97,12 @@ class AddressableLight : public LightOutput, public Component { } virtual ESPColorView get_view_internal(int32_t index) const = 0; - bool effect_active_{false}; ESPColorCorrection correction_{}; + LightState *state_parent_{nullptr}; #ifdef USE_POWER_SUPPLY power_supply::PowerSupplyRequester power_; #endif - LightState *state_parent_{nullptr}; + bool effect_active_{false}; }; class AddressableLightTransformer : public LightTransitionTransformer { @@ -114,9 +114,9 @@ class AddressableLightTransformer : public LightTransitionTransformer { protected: AddressableLight &light_; - Color target_color_{}; float last_transition_progress_{0.0f}; float accumulated_alpha_{0.0f}; + Color target_color_{}; }; } // namespace light diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index c2600d05c2..a3ffe22591 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -2,12 +2,28 @@ #include "light_call.h" #include "light_state.h" #include "esphome/core/log.h" +#include "esphome/core/optional.h" namespace esphome { namespace light { static const char *const TAG = "light"; +// Macro to reduce repetitive setter code +#define IMPLEMENT_LIGHT_CALL_SETTER(name, type, flag) \ + LightCall &LightCall::set_##name(optional(name)) { \ + if ((name).has_value()) { \ + this->name##_ = (name).value(); \ + } \ + this->set_flag_(flag, (name).has_value()); \ + return *this; \ + } \ + LightCall &LightCall::set_##name(type name) { \ + this->name##_ = name; \ + this->set_flag_(flag, true); \ + return *this; \ + } + static const LogString *color_mode_to_human(ColorMode color_mode) { if (color_mode == ColorMode::UNKNOWN) return LOG_STR("Unknown"); @@ -32,41 +48,43 @@ void LightCall::perform() { const char *name = this->parent_->get_name().c_str(); LightColorValues v = this->validate_(); - if (this->publish_) { + if (this->get_publish_()) { ESP_LOGD(TAG, "'%s' Setting:", name); // Only print color mode when it's being changed ColorMode current_color_mode = this->parent_->remote_values.get_color_mode(); - if (this->color_mode_.value_or(current_color_mode) != current_color_mode) { + ColorMode target_color_mode = this->has_color_mode() ? this->color_mode_ : current_color_mode; + if (target_color_mode != current_color_mode) { ESP_LOGD(TAG, " Color mode: %s", LOG_STR_ARG(color_mode_to_human(v.get_color_mode()))); } // Only print state when it's being changed bool current_state = this->parent_->remote_values.is_on(); - if (this->state_.value_or(current_state) != current_state) { + bool target_state = this->has_state() ? this->state_ : current_state; + if (target_state != current_state) { ESP_LOGD(TAG, " State: %s", ONOFF(v.is_on())); } - if (this->brightness_.has_value()) { + if (this->has_brightness()) { ESP_LOGD(TAG, " Brightness: %.0f%%", v.get_brightness() * 100.0f); } - if (this->color_brightness_.has_value()) { + if (this->has_color_brightness()) { ESP_LOGD(TAG, " Color brightness: %.0f%%", v.get_color_brightness() * 100.0f); } - if (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value()) { + if (this->has_red() || this->has_green() || this->has_blue()) { ESP_LOGD(TAG, " Red: %.0f%%, Green: %.0f%%, Blue: %.0f%%", v.get_red() * 100.0f, v.get_green() * 100.0f, v.get_blue() * 100.0f); } - if (this->white_.has_value()) { + if (this->has_white()) { ESP_LOGD(TAG, " White: %.0f%%", v.get_white() * 100.0f); } - if (this->color_temperature_.has_value()) { + if (this->has_color_temperature()) { ESP_LOGD(TAG, " Color temperature: %.1f mireds", v.get_color_temperature()); } - if (this->cold_white_.has_value() || this->warm_white_.has_value()) { + if (this->has_cold_white() || this->has_warm_white()) { ESP_LOGD(TAG, " Cold white: %.0f%%, warm white: %.0f%%", v.get_cold_white() * 100.0f, v.get_warm_white() * 100.0f); } @@ -74,58 +92,57 @@ void LightCall::perform() { if (this->has_flash_()) { // FLASH - if (this->publish_) { - ESP_LOGD(TAG, " Flash length: %.1fs", *this->flash_length_ / 1e3f); + if (this->get_publish_()) { + ESP_LOGD(TAG, " Flash length: %.1fs", this->flash_length_ / 1e3f); } - this->parent_->start_flash_(v, *this->flash_length_, this->publish_); + this->parent_->start_flash_(v, this->flash_length_, this->get_publish_()); } else if (this->has_transition_()) { // TRANSITION - if (this->publish_) { - ESP_LOGD(TAG, " Transition length: %.1fs", *this->transition_length_ / 1e3f); + if (this->get_publish_()) { + ESP_LOGD(TAG, " Transition length: %.1fs", this->transition_length_ / 1e3f); } // Special case: Transition and effect can be set when turning off if (this->has_effect_()) { - if (this->publish_) { + if (this->get_publish_()) { ESP_LOGD(TAG, " Effect: 'None'"); } this->parent_->stop_effect_(); } - this->parent_->start_transition_(v, *this->transition_length_, this->publish_); + this->parent_->start_transition_(v, this->transition_length_, this->get_publish_()); } else if (this->has_effect_()) { // EFFECT - auto effect = this->effect_; const char *effect_s; - if (effect == 0u) { + if (this->effect_ == 0u) { effect_s = "None"; } else { - effect_s = this->parent_->effects_[*this->effect_ - 1]->get_name().c_str(); + effect_s = this->parent_->effects_[this->effect_ - 1]->get_name().c_str(); } - if (this->publish_) { + if (this->get_publish_()) { ESP_LOGD(TAG, " Effect: '%s'", effect_s); } - this->parent_->start_effect_(*this->effect_); + this->parent_->start_effect_(this->effect_); // Also set light color values when starting an effect // For example to turn off the light this->parent_->set_immediately_(v, true); } else { // INSTANT CHANGE - this->parent_->set_immediately_(v, this->publish_); + this->parent_->set_immediately_(v, this->get_publish_()); } if (!this->has_transition_()) { this->parent_->target_state_reached_callback_.call(); } - if (this->publish_) { + if (this->get_publish_()) { this->parent_->publish_state(); } - if (this->save_) { + if (this->get_save_()) { this->parent_->save_remote_values_(); } } @@ -135,82 +152,80 @@ LightColorValues LightCall::validate_() { auto traits = this->parent_->get_traits(); // Color mode check - if (this->color_mode_.has_value() && !traits.supports_color_mode(this->color_mode_.value())) { - ESP_LOGW(TAG, "'%s' - This light does not support color mode %s!", name, - LOG_STR_ARG(color_mode_to_human(this->color_mode_.value()))); - this->color_mode_.reset(); + if (this->has_color_mode() && !traits.supports_color_mode(this->color_mode_)) { + ESP_LOGW(TAG, "'%s' does not support color mode %s", name, LOG_STR_ARG(color_mode_to_human(this->color_mode_))); + this->set_flag_(FLAG_HAS_COLOR_MODE, false); } // Ensure there is always a color mode set - if (!this->color_mode_.has_value()) { + if (!this->has_color_mode()) { this->color_mode_ = this->compute_color_mode_(); + this->set_flag_(FLAG_HAS_COLOR_MODE, true); } - auto color_mode = *this->color_mode_; + auto color_mode = this->color_mode_; // Transform calls that use non-native parameters for the current mode. this->transform_parameters_(); // Brightness exists check - if (this->brightness_.has_value() && *this->brightness_ > 0.0f && !(color_mode & ColorCapability::BRIGHTNESS)) { - ESP_LOGW(TAG, "'%s' - This light does not support setting brightness!", name); - this->brightness_.reset(); + if (this->has_brightness() && this->brightness_ > 0.0f && !(color_mode & ColorCapability::BRIGHTNESS)) { + ESP_LOGW(TAG, "'%s': setting brightness not supported", name); + this->set_flag_(FLAG_HAS_BRIGHTNESS, false); } // Transition length possible check - if (this->transition_length_.has_value() && *this->transition_length_ != 0 && - !(color_mode & ColorCapability::BRIGHTNESS)) { - ESP_LOGW(TAG, "'%s' - This light does not support transitions!", name); - this->transition_length_.reset(); + if (this->has_transition_() && this->transition_length_ != 0 && !(color_mode & ColorCapability::BRIGHTNESS)) { + ESP_LOGW(TAG, "'%s': transitions not supported", name); + this->set_flag_(FLAG_HAS_TRANSITION, false); } // Color brightness exists check - if (this->color_brightness_.has_value() && *this->color_brightness_ > 0.0f && !(color_mode & ColorCapability::RGB)) { - ESP_LOGW(TAG, "'%s' - This color mode does not support setting RGB brightness!", name); - this->color_brightness_.reset(); + if (this->has_color_brightness() && this->color_brightness_ > 0.0f && !(color_mode & ColorCapability::RGB)) { + ESP_LOGW(TAG, "'%s': color mode does not support setting RGB brightness", name); + this->set_flag_(FLAG_HAS_COLOR_BRIGHTNESS, false); } // RGB exists check - if ((this->red_.has_value() && *this->red_ > 0.0f) || (this->green_.has_value() && *this->green_ > 0.0f) || - (this->blue_.has_value() && *this->blue_ > 0.0f)) { + if ((this->has_red() && this->red_ > 0.0f) || (this->has_green() && this->green_ > 0.0f) || + (this->has_blue() && this->blue_ > 0.0f)) { if (!(color_mode & ColorCapability::RGB)) { - ESP_LOGW(TAG, "'%s' - This color mode does not support setting RGB color!", name); - this->red_.reset(); - this->green_.reset(); - this->blue_.reset(); + ESP_LOGW(TAG, "'%s': color mode does not support setting RGB color", name); + this->set_flag_(FLAG_HAS_RED, false); + this->set_flag_(FLAG_HAS_GREEN, false); + this->set_flag_(FLAG_HAS_BLUE, false); } } // White value exists check - if (this->white_.has_value() && *this->white_ > 0.0f && + if (this->has_white() && this->white_ > 0.0f && !(color_mode & ColorCapability::WHITE || color_mode & ColorCapability::COLD_WARM_WHITE)) { - ESP_LOGW(TAG, "'%s' - This color mode does not support setting white value!", name); - this->white_.reset(); + ESP_LOGW(TAG, "'%s': color mode does not support setting white value", name); + this->set_flag_(FLAG_HAS_WHITE, false); } // Color temperature exists check - if (this->color_temperature_.has_value() && + if (this->has_color_temperature() && !(color_mode & ColorCapability::COLOR_TEMPERATURE || color_mode & ColorCapability::COLD_WARM_WHITE)) { - ESP_LOGW(TAG, "'%s' - This color mode does not support setting color temperature!", name); - this->color_temperature_.reset(); + ESP_LOGW(TAG, "'%s': color mode does not support setting color temperature", name); + this->set_flag_(FLAG_HAS_COLOR_TEMPERATURE, false); } // Cold/warm white value exists check - if ((this->cold_white_.has_value() && *this->cold_white_ > 0.0f) || - (this->warm_white_.has_value() && *this->warm_white_ > 0.0f)) { + if ((this->has_cold_white() && this->cold_white_ > 0.0f) || (this->has_warm_white() && this->warm_white_ > 0.0f)) { if (!(color_mode & ColorCapability::COLD_WARM_WHITE)) { - ESP_LOGW(TAG, "'%s' - This color mode does not support setting cold/warm white value!", name); - this->cold_white_.reset(); - this->warm_white_.reset(); + ESP_LOGW(TAG, "'%s': color mode does not support setting cold/warm white value", name); + this->set_flag_(FLAG_HAS_COLD_WHITE, false); + this->set_flag_(FLAG_HAS_WARM_WHITE, false); } } #define VALIDATE_RANGE_(name_, upper_name, min, max) \ - if (name_##_.has_value()) { \ - auto val = *name_##_; \ + if (this->has_##name_()) { \ + auto val = this->name_##_; \ if (val < (min) || val > (max)) { \ - ESP_LOGW(TAG, "'%s' - %s value %.2f is out of range [%.1f - %.1f]!", name, LOG_STR_LITERAL(upper_name), val, \ + ESP_LOGW(TAG, "'%s': %s value %.2f is out of range [%.1f - %.1f]", name, LOG_STR_LITERAL(upper_name), val, \ (min), (max)); \ - name_##_ = clamp(val, (min), (max)); \ + this->name_##_ = clamp(val, (min), (max)); \ } \ } #define VALIDATE_RANGE(name, upper_name) VALIDATE_RANGE_(name, upper_name, 0.0f, 1.0f) @@ -227,110 +242,116 @@ LightColorValues LightCall::validate_() { VALIDATE_RANGE_(color_temperature, "Color temperature", traits.get_min_mireds(), traits.get_max_mireds()) // Flag whether an explicit turn off was requested, in which case we'll also stop the effect. - bool explicit_turn_off_request = this->state_.has_value() && !*this->state_; + bool explicit_turn_off_request = this->has_state() && !this->state_; // Turn off when brightness is set to zero, and reset brightness (so that it has nonzero brightness when turned on). - if (this->brightness_.has_value() && *this->brightness_ == 0.0f) { - this->state_ = optional(false); - this->brightness_ = optional(1.0f); + if (this->has_brightness() && this->brightness_ == 0.0f) { + this->state_ = false; + this->set_flag_(FLAG_HAS_STATE, true); + this->brightness_ = 1.0f; } // Set color brightness to 100% if currently zero and a color is set. - if (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value()) { - if (!this->color_brightness_.has_value() && this->parent_->remote_values.get_color_brightness() == 0.0f) - this->color_brightness_ = optional(1.0f); + if (this->has_red() || this->has_green() || this->has_blue()) { + if (!this->has_color_brightness() && this->parent_->remote_values.get_color_brightness() == 0.0f) { + this->color_brightness_ = 1.0f; + this->set_flag_(FLAG_HAS_COLOR_BRIGHTNESS, true); + } } // Create color values for the light with this call applied. auto v = this->parent_->remote_values; - if (this->color_mode_.has_value()) - v.set_color_mode(*this->color_mode_); - if (this->state_.has_value()) - v.set_state(*this->state_); - if (this->brightness_.has_value()) - v.set_brightness(*this->brightness_); - if (this->color_brightness_.has_value()) - v.set_color_brightness(*this->color_brightness_); - if (this->red_.has_value()) - v.set_red(*this->red_); - if (this->green_.has_value()) - v.set_green(*this->green_); - if (this->blue_.has_value()) - v.set_blue(*this->blue_); - if (this->white_.has_value()) - v.set_white(*this->white_); - if (this->color_temperature_.has_value()) - v.set_color_temperature(*this->color_temperature_); - if (this->cold_white_.has_value()) - v.set_cold_white(*this->cold_white_); - if (this->warm_white_.has_value()) - v.set_warm_white(*this->warm_white_); + if (this->has_color_mode()) + v.set_color_mode(this->color_mode_); + if (this->has_state()) + v.set_state(this->state_); + if (this->has_brightness()) + v.set_brightness(this->brightness_); + if (this->has_color_brightness()) + v.set_color_brightness(this->color_brightness_); + if (this->has_red()) + v.set_red(this->red_); + if (this->has_green()) + v.set_green(this->green_); + if (this->has_blue()) + v.set_blue(this->blue_); + if (this->has_white()) + v.set_white(this->white_); + if (this->has_color_temperature()) + v.set_color_temperature(this->color_temperature_); + if (this->has_cold_white()) + v.set_cold_white(this->cold_white_); + if (this->has_warm_white()) + v.set_warm_white(this->warm_white_); v.normalize_color(); // Flash length check - if (this->has_flash_() && *this->flash_length_ == 0) { - ESP_LOGW(TAG, "'%s' - Flash length must be greater than zero!", name); - this->flash_length_.reset(); + if (this->has_flash_() && this->flash_length_ == 0) { + ESP_LOGW(TAG, "'%s': flash length must be greater than zero", name); + this->set_flag_(FLAG_HAS_FLASH, false); } // validate transition length/flash length/effect not used at the same time bool supports_transition = color_mode & ColorCapability::BRIGHTNESS; // If effect is already active, remove effect start - if (this->has_effect_() && *this->effect_ == this->parent_->active_effect_index_) { - this->effect_.reset(); + if (this->has_effect_() && this->effect_ == this->parent_->active_effect_index_) { + this->set_flag_(FLAG_HAS_EFFECT, false); } // validate effect index - if (this->has_effect_() && *this->effect_ > this->parent_->effects_.size()) { - ESP_LOGW(TAG, "'%s' - Invalid effect index %" PRIu32 "!", name, *this->effect_); - this->effect_.reset(); + if (this->has_effect_() && this->effect_ > this->parent_->effects_.size()) { + ESP_LOGW(TAG, "'%s': invalid effect index %" PRIu32, name, this->effect_); + this->set_flag_(FLAG_HAS_EFFECT, false); } if (this->has_effect_() && (this->has_transition_() || this->has_flash_())) { - ESP_LOGW(TAG, "'%s' - Effect cannot be used together with transition/flash!", name); - this->transition_length_.reset(); - this->flash_length_.reset(); + ESP_LOGW(TAG, "'%s': effect cannot be used with transition/flash", name); + this->set_flag_(FLAG_HAS_TRANSITION, false); + this->set_flag_(FLAG_HAS_FLASH, false); } if (this->has_flash_() && this->has_transition_()) { - ESP_LOGW(TAG, "'%s' - Flash cannot be used together with transition!", name); - this->transition_length_.reset(); + ESP_LOGW(TAG, "'%s': flash cannot be used with transition", name); + this->set_flag_(FLAG_HAS_TRANSITION, false); } - if (!this->has_transition_() && !this->has_flash_() && (!this->has_effect_() || *this->effect_ == 0) && + if (!this->has_transition_() && !this->has_flash_() && (!this->has_effect_() || this->effect_ == 0) && supports_transition) { // nothing specified and light supports transitions, set default transition length this->transition_length_ = this->parent_->default_transition_length_; + this->set_flag_(FLAG_HAS_TRANSITION, true); } - if (this->transition_length_.value_or(0) == 0) { + if (this->has_transition_() && this->transition_length_ == 0) { // 0 transition is interpreted as no transition (instant change) - this->transition_length_.reset(); + this->set_flag_(FLAG_HAS_TRANSITION, false); } if (this->has_transition_() && !supports_transition) { - ESP_LOGW(TAG, "'%s' - Light does not support transitions!", name); - this->transition_length_.reset(); + ESP_LOGW(TAG, "'%s': transitions not supported", name); + this->set_flag_(FLAG_HAS_TRANSITION, false); } // If not a flash and turning the light off, then disable the light // Do not use light color values directly, so that effects can set 0% brightness // Reason: When user turns off the light in frontend, the effect should also stop - if (!this->has_flash_() && !this->state_.value_or(v.is_on())) { + bool target_state = this->has_state() ? this->state_ : v.is_on(); + if (!this->has_flash_() && !target_state) { if (this->has_effect_()) { - ESP_LOGW(TAG, "'%s' - Cannot start an effect when turning off!", name); - this->effect_.reset(); + ESP_LOGW(TAG, "'%s': cannot start effect when turning off", name); + this->set_flag_(FLAG_HAS_EFFECT, false); } else if (this->parent_->active_effect_index_ != 0 && explicit_turn_off_request) { // Auto turn off effect this->effect_ = 0; + this->set_flag_(FLAG_HAS_EFFECT, true); } } // Disable saving for flashes if (this->has_flash_()) - this->save_ = false; + this->set_flag_(FLAG_SAVE, false); return v; } @@ -343,24 +364,27 @@ void LightCall::transform_parameters_() { // - RGBWW lights with color_interlock=true, which also sets "brightness" and // "color_temperature" (without color_interlock, CW/WW are set directly) // - Legacy Home Assistant (pre-colormode), which sets "white" and "color_temperature" - if (((this->white_.has_value() && *this->white_ > 0.0f) || this->color_temperature_.has_value()) && // - (*this->color_mode_ & ColorCapability::COLD_WARM_WHITE) && // - !(*this->color_mode_ & ColorCapability::WHITE) && // - !(*this->color_mode_ & ColorCapability::COLOR_TEMPERATURE) && // + if (((this->has_white() && this->white_ > 0.0f) || this->has_color_temperature()) && // + (this->color_mode_ & ColorCapability::COLD_WARM_WHITE) && // + !(this->color_mode_ & ColorCapability::WHITE) && // + !(this->color_mode_ & ColorCapability::COLOR_TEMPERATURE) && // traits.get_min_mireds() > 0.0f && traits.get_max_mireds() > 0.0f) { - ESP_LOGD(TAG, "'%s' - Setting cold/warm white channels using white/color temperature values.", + ESP_LOGD(TAG, "'%s': setting cold/warm white channels using white/color temperature values", this->parent_->get_name().c_str()); - if (this->color_temperature_.has_value()) { - const float color_temp = clamp(*this->color_temperature_, traits.get_min_mireds(), traits.get_max_mireds()); + if (this->has_color_temperature()) { + const float color_temp = clamp(this->color_temperature_, traits.get_min_mireds(), traits.get_max_mireds()); const float ww_fraction = (color_temp - traits.get_min_mireds()) / (traits.get_max_mireds() - traits.get_min_mireds()); const float cw_fraction = 1.0f - ww_fraction; const float max_cw_ww = std::max(ww_fraction, cw_fraction); this->cold_white_ = gamma_uncorrect(cw_fraction / max_cw_ww, this->parent_->get_gamma_correct()); this->warm_white_ = gamma_uncorrect(ww_fraction / max_cw_ww, this->parent_->get_gamma_correct()); + this->set_flag_(FLAG_HAS_COLD_WHITE, true); + this->set_flag_(FLAG_HAS_WARM_WHITE, true); } - if (this->white_.has_value()) { - this->brightness_ = *this->white_; + if (this->has_white()) { + this->brightness_ = this->white_; + this->set_flag_(FLAG_HAS_BRIGHTNESS, true); } } } @@ -378,7 +402,7 @@ ColorMode LightCall::compute_color_mode_() { // Don't change if the light is being turned off. ColorMode current_mode = this->parent_->remote_values.get_color_mode(); - if (this->state_.has_value() && !*this->state_) + if (this->has_state() && !this->state_) return current_mode; // If no color mode is specified, we try to guess the color mode. This is needed for backward compatibility to @@ -388,8 +412,8 @@ ColorMode LightCall::compute_color_mode_() { // Don't change if the current mode is suitable. if (suitable_modes.count(current_mode) > 0) { - ESP_LOGI(TAG, "'%s' - Keeping current color mode %s for call without color mode.", - this->parent_->get_name().c_str(), LOG_STR_ARG(color_mode_to_human(current_mode))); + ESP_LOGI(TAG, "'%s': color mode not specified; retaining %s", this->parent_->get_name().c_str(), + LOG_STR_ARG(color_mode_to_human(current_mode))); return current_mode; } @@ -398,7 +422,7 @@ ColorMode LightCall::compute_color_mode_() { if (supported_modes.count(mode) == 0) continue; - ESP_LOGI(TAG, "'%s' - Using color mode %s for call without color mode.", this->parent_->get_name().c_str(), + ESP_LOGI(TAG, "'%s': color mode not specified; using %s", this->parent_->get_name().c_str(), LOG_STR_ARG(color_mode_to_human(mode))); return mode; } @@ -406,17 +430,17 @@ ColorMode LightCall::compute_color_mode_() { // There's no supported mode for this call, so warn, use the current more or a mode at random and let validation strip // out whatever we don't support. auto color_mode = current_mode != ColorMode::UNKNOWN ? current_mode : *supported_modes.begin(); - ESP_LOGW(TAG, "'%s' - No color mode suitable for this call supported, defaulting to %s!", - this->parent_->get_name().c_str(), LOG_STR_ARG(color_mode_to_human(color_mode))); + ESP_LOGW(TAG, "'%s': no suitable color mode supported; defaulting to %s", this->parent_->get_name().c_str(), + LOG_STR_ARG(color_mode_to_human(color_mode))); return color_mode; } std::set LightCall::get_suitable_color_modes_() { - bool has_white = this->white_.has_value() && *this->white_ > 0.0f; - bool has_ct = this->color_temperature_.has_value(); - bool has_cwww = (this->cold_white_.has_value() && *this->cold_white_ > 0.0f) || - (this->warm_white_.has_value() && *this->warm_white_ > 0.0f); - bool has_rgb = (this->color_brightness_.has_value() && *this->color_brightness_ > 0.0f) || - (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value()); + bool has_white = this->has_white() && this->white_ > 0.0f; + bool has_ct = this->has_color_temperature(); + bool has_cwww = + (this->has_cold_white() && this->cold_white_ > 0.0f) || (this->has_warm_white() && this->warm_white_ > 0.0f); + bool has_rgb = (this->has_color_brightness() && this->color_brightness_ > 0.0f) || + (this->has_red() || this->has_green() || this->has_blue()); #define KEY(white, ct, cwww, rgb) ((white) << 0 | (ct) << 1 | (cwww) << 2 | (rgb) << 3) #define ENTRY(white, ct, cwww, rgb, ...) \ @@ -472,7 +496,7 @@ LightCall &LightCall::set_effect(const std::string &effect) { } } if (!found) { - ESP_LOGW(TAG, "'%s' - No such effect '%s'", this->parent_->get_name().c_str(), effect.c_str()); + ESP_LOGW(TAG, "'%s': no such effect '%s'", this->parent_->get_name().c_str(), effect.c_str()); } return *this; } @@ -491,7 +515,7 @@ LightCall &LightCall::from_light_color_values(const LightColorValues &values) { return *this; } ColorMode LightCall::get_active_color_mode_() { - return this->color_mode_.value_or(this->parent_->remote_values.get_color_mode()); + return this->has_color_mode() ? this->color_mode_ : this->parent_->remote_values.get_color_mode(); } LightCall &LightCall::set_transition_length_if_supported(uint32_t transition_length) { if (this->get_active_color_mode_() & ColorCapability::BRIGHTNESS) @@ -505,7 +529,7 @@ LightCall &LightCall::set_brightness_if_supported(float brightness) { } LightCall &LightCall::set_color_mode_if_supported(ColorMode color_mode) { if (this->parent_->get_traits().supports_color_mode(color_mode)) - this->color_mode_ = color_mode; + this->set_color_mode(color_mode); return *this; } LightCall &LightCall::set_color_brightness_if_supported(float brightness) { @@ -549,110 +573,19 @@ LightCall &LightCall::set_warm_white_if_supported(float warm_white) { this->set_warm_white(warm_white); return *this; } -LightCall &LightCall::set_state(optional state) { - this->state_ = state; - return *this; -} -LightCall &LightCall::set_state(bool state) { - this->state_ = state; - return *this; -} -LightCall &LightCall::set_transition_length(optional transition_length) { - this->transition_length_ = transition_length; - return *this; -} -LightCall &LightCall::set_transition_length(uint32_t transition_length) { - this->transition_length_ = transition_length; - return *this; -} -LightCall &LightCall::set_flash_length(optional flash_length) { - this->flash_length_ = flash_length; - return *this; -} -LightCall &LightCall::set_flash_length(uint32_t flash_length) { - this->flash_length_ = flash_length; - return *this; -} -LightCall &LightCall::set_brightness(optional brightness) { - this->brightness_ = brightness; - return *this; -} -LightCall &LightCall::set_brightness(float brightness) { - this->brightness_ = brightness; - return *this; -} -LightCall &LightCall::set_color_mode(optional color_mode) { - this->color_mode_ = color_mode; - return *this; -} -LightCall &LightCall::set_color_mode(ColorMode color_mode) { - this->color_mode_ = color_mode; - return *this; -} -LightCall &LightCall::set_color_brightness(optional brightness) { - this->color_brightness_ = brightness; - return *this; -} -LightCall &LightCall::set_color_brightness(float brightness) { - this->color_brightness_ = brightness; - return *this; -} -LightCall &LightCall::set_red(optional red) { - this->red_ = red; - return *this; -} -LightCall &LightCall::set_red(float red) { - this->red_ = red; - return *this; -} -LightCall &LightCall::set_green(optional green) { - this->green_ = green; - return *this; -} -LightCall &LightCall::set_green(float green) { - this->green_ = green; - return *this; -} -LightCall &LightCall::set_blue(optional blue) { - this->blue_ = blue; - return *this; -} -LightCall &LightCall::set_blue(float blue) { - this->blue_ = blue; - return *this; -} -LightCall &LightCall::set_white(optional white) { - this->white_ = white; - return *this; -} -LightCall &LightCall::set_white(float white) { - this->white_ = white; - return *this; -} -LightCall &LightCall::set_color_temperature(optional color_temperature) { - this->color_temperature_ = color_temperature; - return *this; -} -LightCall &LightCall::set_color_temperature(float color_temperature) { - this->color_temperature_ = color_temperature; - return *this; -} -LightCall &LightCall::set_cold_white(optional cold_white) { - this->cold_white_ = cold_white; - return *this; -} -LightCall &LightCall::set_cold_white(float cold_white) { - this->cold_white_ = cold_white; - return *this; -} -LightCall &LightCall::set_warm_white(optional warm_white) { - this->warm_white_ = warm_white; - return *this; -} -LightCall &LightCall::set_warm_white(float warm_white) { - this->warm_white_ = warm_white; - return *this; -} +IMPLEMENT_LIGHT_CALL_SETTER(state, bool, FLAG_HAS_STATE) +IMPLEMENT_LIGHT_CALL_SETTER(transition_length, uint32_t, FLAG_HAS_TRANSITION) +IMPLEMENT_LIGHT_CALL_SETTER(flash_length, uint32_t, FLAG_HAS_FLASH) +IMPLEMENT_LIGHT_CALL_SETTER(brightness, float, FLAG_HAS_BRIGHTNESS) +IMPLEMENT_LIGHT_CALL_SETTER(color_mode, ColorMode, FLAG_HAS_COLOR_MODE) +IMPLEMENT_LIGHT_CALL_SETTER(color_brightness, float, FLAG_HAS_COLOR_BRIGHTNESS) +IMPLEMENT_LIGHT_CALL_SETTER(red, float, FLAG_HAS_RED) +IMPLEMENT_LIGHT_CALL_SETTER(green, float, FLAG_HAS_GREEN) +IMPLEMENT_LIGHT_CALL_SETTER(blue, float, FLAG_HAS_BLUE) +IMPLEMENT_LIGHT_CALL_SETTER(white, float, FLAG_HAS_WHITE) +IMPLEMENT_LIGHT_CALL_SETTER(color_temperature, float, FLAG_HAS_COLOR_TEMPERATURE) +IMPLEMENT_LIGHT_CALL_SETTER(cold_white, float, FLAG_HAS_COLD_WHITE) +IMPLEMENT_LIGHT_CALL_SETTER(warm_white, float, FLAG_HAS_WARM_WHITE) LightCall &LightCall::set_effect(optional effect) { if (effect.has_value()) this->set_effect(*effect); @@ -660,18 +593,22 @@ LightCall &LightCall::set_effect(optional effect) { } LightCall &LightCall::set_effect(uint32_t effect_number) { this->effect_ = effect_number; + this->set_flag_(FLAG_HAS_EFFECT, true); return *this; } LightCall &LightCall::set_effect(optional effect_number) { - this->effect_ = effect_number; + if (effect_number.has_value()) { + this->effect_ = effect_number.value(); + } + this->set_flag_(FLAG_HAS_EFFECT, effect_number.has_value()); return *this; } LightCall &LightCall::set_publish(bool publish) { - this->publish_ = publish; + this->set_flag_(FLAG_PUBLISH, publish); return *this; } LightCall &LightCall::set_save(bool save) { - this->save_ = save; + this->set_flag_(FLAG_SAVE, save); return *this; } LightCall &LightCall::set_rgb(float red, float green, float blue) { diff --git a/esphome/components/light/light_call.h b/esphome/components/light/light_call.h index bca2ac7b07..7e04e1a767 100644 --- a/esphome/components/light/light_call.h +++ b/esphome/components/light/light_call.h @@ -1,6 +1,5 @@ #pragma once -#include "esphome/core/optional.h" #include "light_color_values.h" #include @@ -10,6 +9,11 @@ namespace light { class LightState; /** This class represents a requested change in a light state. + * + * Light state changes are tracked using a bitfield flags_ to minimize memory usage. + * Each possible light property has a flag indicating whether it has been set. + * This design keeps LightCall at ~56 bytes to minimize heap fragmentation on + * ESP8266 and other memory-constrained devices. */ class LightCall { public: @@ -131,6 +135,19 @@ class LightCall { /// Set whether this light call should trigger a save state to recover them at startup.. LightCall &set_save(bool save); + // Getter methods to check if values are set + bool has_state() const { return (flags_ & FLAG_HAS_STATE) != 0; } + bool has_brightness() const { return (flags_ & FLAG_HAS_BRIGHTNESS) != 0; } + bool has_color_brightness() const { return (flags_ & FLAG_HAS_COLOR_BRIGHTNESS) != 0; } + bool has_red() const { return (flags_ & FLAG_HAS_RED) != 0; } + bool has_green() const { return (flags_ & FLAG_HAS_GREEN) != 0; } + bool has_blue() const { return (flags_ & FLAG_HAS_BLUE) != 0; } + bool has_white() const { return (flags_ & FLAG_HAS_WHITE) != 0; } + bool has_color_temperature() const { return (flags_ & FLAG_HAS_COLOR_TEMPERATURE) != 0; } + bool has_cold_white() const { return (flags_ & FLAG_HAS_COLD_WHITE) != 0; } + bool has_warm_white() const { return (flags_ & FLAG_HAS_WARM_WHITE) != 0; } + bool has_color_mode() const { return (flags_ & FLAG_HAS_COLOR_MODE) != 0; } + /** Set the RGB color of the light by RGB values. * * Please note that this only changes the color of the light, not the brightness. @@ -170,27 +187,62 @@ class LightCall { /// Some color modes also can be set using non-native parameters, transform those calls. void transform_parameters_(); - bool has_transition_() { return this->transition_length_.has_value(); } - bool has_flash_() { return this->flash_length_.has_value(); } - bool has_effect_() { return this->effect_.has_value(); } + // Bitfield flags - each flag indicates whether a corresponding value has been set. + enum FieldFlags : uint16_t { + FLAG_HAS_STATE = 1 << 0, + FLAG_HAS_TRANSITION = 1 << 1, + FLAG_HAS_FLASH = 1 << 2, + FLAG_HAS_EFFECT = 1 << 3, + FLAG_HAS_BRIGHTNESS = 1 << 4, + FLAG_HAS_COLOR_BRIGHTNESS = 1 << 5, + FLAG_HAS_RED = 1 << 6, + FLAG_HAS_GREEN = 1 << 7, + FLAG_HAS_BLUE = 1 << 8, + FLAG_HAS_WHITE = 1 << 9, + FLAG_HAS_COLOR_TEMPERATURE = 1 << 10, + FLAG_HAS_COLD_WHITE = 1 << 11, + FLAG_HAS_WARM_WHITE = 1 << 12, + FLAG_HAS_COLOR_MODE = 1 << 13, + FLAG_PUBLISH = 1 << 14, + FLAG_SAVE = 1 << 15, + }; + + bool has_transition_() { return (this->flags_ & FLAG_HAS_TRANSITION) != 0; } + bool has_flash_() { return (this->flags_ & FLAG_HAS_FLASH) != 0; } + bool has_effect_() { return (this->flags_ & FLAG_HAS_EFFECT) != 0; } + bool get_publish_() { return (this->flags_ & FLAG_PUBLISH) != 0; } + bool get_save_() { return (this->flags_ & FLAG_SAVE) != 0; } + + // Helper to set flag + void set_flag_(FieldFlags flag, bool value) { + if (value) { + this->flags_ |= flag; + } else { + this->flags_ &= ~flag; + } + } LightState *parent_; - optional state_; - optional transition_length_; - optional flash_length_; - optional color_mode_; - optional brightness_; - optional color_brightness_; - optional red_; - optional green_; - optional blue_; - optional white_; - optional color_temperature_; - optional cold_white_; - optional warm_white_; - optional effect_; - bool publish_{true}; - bool save_{true}; + + // Light state values - use flags_ to check if a value has been set. + // Group 4-byte aligned members first + uint32_t transition_length_; + uint32_t flash_length_; + uint32_t effect_; + float brightness_; + float color_brightness_; + float red_; + float green_; + float blue_; + float white_; + float color_temperature_; + float cold_white_; + float warm_white_; + + // Smaller members at the end for better packing + uint16_t flags_{FLAG_PUBLISH | FLAG_SAVE}; // Tracks which values are set + ColorMode color_mode_; + bool state_; }; } // namespace light diff --git a/esphome/components/light/light_color_values.h b/esphome/components/light/light_color_values.h index ca32b9c571..5653a8d2a5 100644 --- a/esphome/components/light/light_color_values.h +++ b/esphome/components/light/light_color_values.h @@ -46,8 +46,7 @@ class LightColorValues { public: /// Construct the LightColorValues with all attributes enabled, but state set to off. LightColorValues() - : color_mode_(ColorMode::UNKNOWN), - state_(0.0f), + : state_(0.0f), brightness_(1.0f), color_brightness_(1.0f), red_(1.0f), @@ -56,7 +55,8 @@ class LightColorValues { white_(1.0f), color_temperature_{0.0f}, cold_white_{1.0f}, - warm_white_{1.0f} {} + warm_white_{1.0f}, + color_mode_(ColorMode::UNKNOWN) {} LightColorValues(ColorMode color_mode, float state, float brightness, float color_brightness, float red, float green, float blue, float white, float color_temperature, float cold_white, float warm_white) { @@ -86,16 +86,16 @@ class LightColorValues { static LightColorValues lerp(const LightColorValues &start, const LightColorValues &end, float completion) { LightColorValues v; v.set_color_mode(end.color_mode_); - v.set_state(esphome::lerp(completion, start.get_state(), end.get_state())); - v.set_brightness(esphome::lerp(completion, start.get_brightness(), end.get_brightness())); - v.set_color_brightness(esphome::lerp(completion, start.get_color_brightness(), end.get_color_brightness())); - v.set_red(esphome::lerp(completion, start.get_red(), end.get_red())); - v.set_green(esphome::lerp(completion, start.get_green(), end.get_green())); - v.set_blue(esphome::lerp(completion, start.get_blue(), end.get_blue())); - v.set_white(esphome::lerp(completion, start.get_white(), end.get_white())); - v.set_color_temperature(esphome::lerp(completion, start.get_color_temperature(), end.get_color_temperature())); - v.set_cold_white(esphome::lerp(completion, start.get_cold_white(), end.get_cold_white())); - v.set_warm_white(esphome::lerp(completion, start.get_warm_white(), end.get_warm_white())); + v.set_state(std::lerp(start.get_state(), end.get_state(), completion)); + v.set_brightness(std::lerp(start.get_brightness(), end.get_brightness(), completion)); + v.set_color_brightness(std::lerp(start.get_color_brightness(), end.get_color_brightness(), completion)); + v.set_red(std::lerp(start.get_red(), end.get_red(), completion)); + v.set_green(std::lerp(start.get_green(), end.get_green(), completion)); + v.set_blue(std::lerp(start.get_blue(), end.get_blue(), completion)); + v.set_white(std::lerp(start.get_white(), end.get_white(), completion)); + v.set_color_temperature(std::lerp(start.get_color_temperature(), end.get_color_temperature(), completion)); + v.set_cold_white(std::lerp(start.get_cold_white(), end.get_cold_white(), completion)); + v.set_warm_white(std::lerp(start.get_warm_white(), end.get_warm_white(), completion)); return v; } @@ -292,7 +292,6 @@ class LightColorValues { void set_warm_white(float warm_white) { this->warm_white_ = clamp(warm_white, 0.0f, 1.0f); } protected: - ColorMode color_mode_; float state_; ///< ON / OFF, float for transition float brightness_; float color_brightness_; @@ -303,6 +302,7 @@ class LightColorValues { float color_temperature_; ///< Color Temperature in Mired float cold_white_; float warm_white_; + ColorMode color_mode_; }; } // namespace light diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index b93823feac..72cb99223e 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -31,9 +31,7 @@ enum LightRestoreMode : uint8_t { struct LightStateRTCState { LightStateRTCState(ColorMode color_mode, bool state, float brightness, float color_brightness, float red, float green, float blue, float white, float color_temp, float cold_white, float warm_white) - : color_mode(color_mode), - state(state), - brightness(brightness), + : brightness(brightness), color_brightness(color_brightness), red(red), green(green), @@ -41,10 +39,12 @@ struct LightStateRTCState { white(white), color_temp(color_temp), cold_white(cold_white), - warm_white(warm_white) {} + warm_white(warm_white), + effect(0), + color_mode(color_mode), + state(state) {} LightStateRTCState() = default; - ColorMode color_mode{ColorMode::UNKNOWN}; - bool state{false}; + // Group 4-byte aligned members first float brightness{1.0f}; float color_brightness{1.0f}; float red{1.0f}; @@ -55,6 +55,9 @@ struct LightStateRTCState { float cold_white{1.0f}; float warm_white{1.0f}; uint32_t effect{0}; + // Group smaller members at the end + ColorMode color_mode{ColorMode::UNKNOWN}; + bool state{false}; }; /** This class represents the communication layer between the front-end MQTT layer and the @@ -216,6 +219,8 @@ class LightState : public EntityBase, public Component { std::unique_ptr transformer_{nullptr}; /// List of effects for this light. std::vector effects_; + /// Object used to store the persisted values of the light. + ESPPreferenceObject rtc_; /// Value for storing the index of the currently active effect. 0 if no effect is active uint32_t active_effect_index_{}; /// Default transition length for all transitions in ms. @@ -224,9 +229,10 @@ class LightState : public EntityBase, public Component { uint32_t flash_transition_length_{}; /// Gamma correction factor for the light. float gamma_correct_{}; - - /// Object used to store the persisted values of the light. - ESPPreferenceObject rtc_; + /// Whether the light value should be written in the next cycle. + bool next_write_{true}; + // for effects, true if a transformer (transition) is active. + bool is_transformer_active_ = false; /** Callback to call when new values for the frontend are available. * @@ -247,10 +253,6 @@ class LightState : public EntityBase, public Component { /// Restore mode of the light. LightRestoreMode restore_mode_; - /// Whether the light value should be written in the next cycle. - bool next_write_{true}; - // for effects, true if a transformer (transition) is active. - bool is_transformer_active_ = false; }; } // namespace light diff --git a/esphome/components/light/transformers.h b/esphome/components/light/transformers.h index a557bd39b1..8d49acff97 100644 --- a/esphome/components/light/transformers.h +++ b/esphome/components/light/transformers.h @@ -59,9 +59,9 @@ class LightTransitionTransformer : public LightTransformer { // transition from 0 to 1 on x = [0, 1] static float smoothed_progress(float x) { return x * x * x * (x * (x * 6.0f - 15.0f) + 10.0f); } - bool changing_color_mode_{false}; LightColorValues end_values_{}; LightColorValues intermediate_values_{}; + bool changing_color_mode_{false}; }; class LightFlashTransformer : public LightTransformer { @@ -117,8 +117,8 @@ class LightFlashTransformer : public LightTransformer { protected: LightState &state_; - uint32_t transition_length_; std::unique_ptr transformer_{nullptr}; + uint32_t transition_length_; bool begun_lightstate_restore_; }; diff --git a/esphome/components/ln882x/__init__.py b/esphome/components/ln882x/__init__.py new file mode 100644 index 0000000000..6a76218f87 --- /dev/null +++ b/esphome/components/ln882x/__init__.py @@ -0,0 +1,52 @@ +# This file was auto-generated by libretiny/generate_components.py +# Do not modify its contents. +# For custom pin validators, put validate_pin() or validate_usage() +# in gpio.py file in this directory. +# For changing schema/pin schema, put COMPONENT_SCHEMA or COMPONENT_PIN_SCHEMA +# in schema.py file in this directory. + +from esphome import pins +from esphome.components import libretiny +from esphome.components.libretiny.const import ( + COMPONENT_LN882X, + KEY_COMPONENT_DATA, + KEY_LIBRETINY, + LibreTinyComponent, +) +from esphome.core import CORE + +from .boards import LN882X_BOARD_PINS, LN882X_BOARDS + +CODEOWNERS = ["@lamauny"] +AUTO_LOAD = ["libretiny"] +IS_TARGET_PLATFORM = True + +COMPONENT_DATA = LibreTinyComponent( + name=COMPONENT_LN882X, + boards=LN882X_BOARDS, + board_pins=LN882X_BOARD_PINS, + pin_validation=None, + usage_validation=None, +) + + +def _set_core_data(config): + CORE.data[KEY_LIBRETINY] = {} + CORE.data[KEY_LIBRETINY][KEY_COMPONENT_DATA] = COMPONENT_DATA + return config + + +CONFIG_SCHEMA = libretiny.BASE_SCHEMA + +PIN_SCHEMA = libretiny.gpio.BASE_PIN_SCHEMA + +CONFIG_SCHEMA.prepend_extra(_set_core_data) + + +async def to_code(config): + return await libretiny.component_to_code(config) + + +@pins.PIN_SCHEMA_REGISTRY.register("ln882x", PIN_SCHEMA) +async def pin_to_code(config): + return await libretiny.gpio.component_pin_to_code(config) diff --git a/esphome/components/ln882x/boards.py b/esphome/components/ln882x/boards.py new file mode 100644 index 0000000000..43f25994a7 --- /dev/null +++ b/esphome/components/ln882x/boards.py @@ -0,0 +1,285 @@ +# This file was auto-generated by libretiny/generate_components.py +# Do not modify its contents. + +from esphome.components.libretiny.const import FAMILY_LN882H + +LN882X_BOARDS = { + "wl2s": { + "name": "WL2S Wi-Fi/BLE Module", + "family": FAMILY_LN882H, + }, + "ln-02": { + "name": "LN-02 Wi-Fi/BLE Module", + "family": FAMILY_LN882H, + }, + "generic-ln882hki": { + "name": "Generic - LN882HKI", + "family": FAMILY_LN882H, + }, +} + +LN882X_BOARD_PINS = { + "wl2s": { + "WIRE0_SCL_0": 7, + "WIRE0_SCL_1": 12, + "WIRE0_SCL_2": 3, + "WIRE0_SCL_3": 10, + "WIRE0_SCL_4": 2, + "WIRE0_SCL_5": 0, + "WIRE0_SCL_6": 19, + "WIRE0_SCL_7": 11, + "WIRE0_SCL_8": 9, + "WIRE0_SCL_9": 24, + "WIRE0_SCL_10": 25, + "WIRE0_SCL_11": 5, + "WIRE0_SCL_12": 1, + "WIRE0_SDA_0": 7, + "WIRE0_SDA_1": 12, + "WIRE0_SDA_2": 3, + "WIRE0_SDA_3": 10, + "WIRE0_SDA_4": 2, + "WIRE0_SDA_5": 0, + "WIRE0_SDA_6": 19, + "WIRE0_SDA_7": 11, + "WIRE0_SDA_8": 9, + "WIRE0_SDA_9": 24, + "WIRE0_SDA_10": 25, + "WIRE0_SDA_11": 5, + "WIRE0_SDA_12": 1, + "SERIAL0_RX": 3, + "SERIAL0_TX": 2, + "SERIAL1_RX": 24, + "SERIAL1_TX": 25, + "ADC2": 0, + "ADC3": 1, + "ADC5": 19, + "PA00": 0, + "PA0": 0, + "PA01": 1, + "PA1": 1, + "PA02": 2, + "PA2": 2, + "PA03": 3, + "PA3": 3, + "PA05": 5, + "PA5": 5, + "PA07": 7, + "PA7": 7, + "PA09": 9, + "PA9": 9, + "PA10": 10, + "PA11": 11, + "PA12": 12, + "PB03": 19, + "PB3": 19, + "PB08": 24, + "PB8": 24, + "PB09": 25, + "PB9": 25, + "RX0": 3, + "RX1": 24, + "SCL0": 1, + "SDA0": 1, + "TX0": 2, + "TX1": 25, + "D0": 7, + "D1": 12, + "D2": 3, + "D3": 10, + "D4": 2, + "D5": 0, + "D6": 19, + "D7": 11, + "D8": 9, + "D9": 24, + "D10": 25, + "D11": 5, + "D12": 1, + "A0": 0, + "A1": 19, + "A2": 1, + }, + "ln-02": { + "WIRE0_SCL_0": 11, + "WIRE0_SCL_1": 19, + "WIRE0_SCL_2": 3, + "WIRE0_SCL_3": 24, + "WIRE0_SCL_4": 2, + "WIRE0_SCL_5": 25, + "WIRE0_SCL_6": 1, + "WIRE0_SCL_7": 0, + "WIRE0_SCL_8": 9, + "WIRE0_SDA_0": 11, + "WIRE0_SDA_1": 19, + "WIRE0_SDA_2": 3, + "WIRE0_SDA_3": 24, + "WIRE0_SDA_4": 2, + "WIRE0_SDA_5": 25, + "WIRE0_SDA_6": 1, + "WIRE0_SDA_7": 0, + "WIRE0_SDA_8": 9, + "SERIAL0_RX": 3, + "SERIAL0_TX": 2, + "SERIAL1_RX": 24, + "SERIAL1_TX": 25, + "ADC2": 0, + "ADC3": 1, + "ADC5": 19, + "PA00": 0, + "PA0": 0, + "PA01": 1, + "PA1": 1, + "PA02": 2, + "PA2": 2, + "PA03": 3, + "PA3": 3, + "PA09": 9, + "PA9": 9, + "PA11": 11, + "PB03": 19, + "PB3": 19, + "PB08": 24, + "PB8": 24, + "PB09": 25, + "PB9": 25, + "RX0": 3, + "RX1": 24, + "SCL0": 9, + "SDA0": 9, + "TX0": 2, + "TX1": 25, + "D0": 11, + "D1": 19, + "D2": 3, + "D3": 24, + "D4": 2, + "D5": 25, + "D6": 1, + "D7": 0, + "D8": 9, + "A0": 19, + "A1": 1, + "A2": 0, + }, + "generic-ln882hki": { + "WIRE0_SCL_0": 0, + "WIRE0_SCL_1": 1, + "WIRE0_SCL_2": 2, + "WIRE0_SCL_3": 3, + "WIRE0_SCL_4": 4, + "WIRE0_SCL_5": 5, + "WIRE0_SCL_6": 6, + "WIRE0_SCL_7": 7, + "WIRE0_SCL_8": 8, + "WIRE0_SCL_9": 9, + "WIRE0_SCL_10": 10, + "WIRE0_SCL_11": 11, + "WIRE0_SCL_12": 12, + "WIRE0_SCL_13": 19, + "WIRE0_SCL_14": 20, + "WIRE0_SCL_15": 21, + "WIRE0_SCL_16": 22, + "WIRE0_SCL_17": 23, + "WIRE0_SCL_18": 24, + "WIRE0_SCL_19": 25, + "WIRE0_SDA_0": 0, + "WIRE0_SDA_1": 1, + "WIRE0_SDA_2": 2, + "WIRE0_SDA_3": 3, + "WIRE0_SDA_4": 4, + "WIRE0_SDA_5": 5, + "WIRE0_SDA_6": 6, + "WIRE0_SDA_7": 7, + "WIRE0_SDA_8": 8, + "WIRE0_SDA_9": 9, + "WIRE0_SDA_10": 10, + "WIRE0_SDA_11": 11, + "WIRE0_SDA_12": 12, + "WIRE0_SDA_13": 19, + "WIRE0_SDA_14": 20, + "WIRE0_SDA_15": 21, + "WIRE0_SDA_16": 22, + "WIRE0_SDA_17": 23, + "WIRE0_SDA_18": 24, + "WIRE0_SDA_19": 25, + "SERIAL0_RX": 3, + "SERIAL0_TX": 2, + "SERIAL1_RX": 24, + "SERIAL1_TX": 25, + "ADC2": 0, + "ADC3": 1, + "ADC4": 4, + "ADC5": 19, + "ADC6": 20, + "ADC7": 21, + "PA00": 0, + "PA0": 0, + "PA01": 1, + "PA1": 1, + "PA02": 2, + "PA2": 2, + "PA03": 3, + "PA3": 3, + "PA04": 4, + "PA4": 4, + "PA05": 5, + "PA5": 5, + "PA06": 6, + "PA6": 6, + "PA07": 7, + "PA7": 7, + "PA08": 8, + "PA8": 8, + "PA09": 9, + "PA9": 9, + "PA10": 10, + "PA11": 11, + "PA12": 12, + "PB03": 19, + "PB3": 19, + "PB04": 20, + "PB4": 20, + "PB05": 21, + "PB5": 21, + "PB06": 22, + "PB6": 22, + "PB07": 23, + "PB7": 23, + "PB08": 24, + "PB8": 24, + "PB09": 25, + "PB9": 25, + "RX0": 3, + "RX1": 24, + "TX0": 2, + "TX1": 25, + "D0": 0, + "D1": 1, + "D2": 2, + "D3": 3, + "D4": 4, + "D5": 5, + "D6": 6, + "D7": 7, + "D8": 8, + "D9": 9, + "D10": 10, + "D11": 11, + "D12": 12, + "D13": 19, + "D14": 20, + "D15": 21, + "D16": 22, + "D17": 23, + "D18": 24, + "D19": 25, + "A2": 0, + "A3": 1, + "A4": 4, + "A5": 19, + "A6": 20, + "A7": 21, + }, +} + +BOARDS = LN882X_BOARDS diff --git a/esphome/components/lock/__init__.py b/esphome/components/lock/__init__.py index 0fb67e3948..e62d9f3e2b 100644 --- a/esphome/components/lock/__init__.py +++ b/esphome/components/lock/__init__.py @@ -14,8 +14,8 @@ from esphome.const import ( CONF_WEB_SERVER, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@esphome/core"] IS_PLATFORM_COMPONENT = True @@ -67,6 +67,9 @@ _LOCK_SCHEMA = ( ) +_LOCK_SCHEMA.add_extra(entity_duplicate_validator("lock")) + + def lock_schema( class_: MockObjClass = cv.UNDEFINED, *, @@ -94,7 +97,7 @@ LOCK_SCHEMA.add_extra(cv.deprecated_schema_constant("lock")) async def _setup_lock_core(var, config): - await setup_entity(var, config) + await setup_entity(var, config, "lock") for conf in config.get(CONF_ON_LOCK, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 462cae73b6..9ac2999696 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -16,7 +16,12 @@ from esphome.components.esp32.const import ( VARIANT_ESP32S3, ) from esphome.components.libretiny import get_libretiny_component, get_libretiny_family -from esphome.components.libretiny.const import COMPONENT_BK72XX, COMPONENT_RTL87XX +from esphome.components.libretiny.const import ( + COMPONENT_BK72XX, + COMPONENT_LN882X, + COMPONENT_RTL87XX, +) +from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( CONF_ARGS, @@ -35,8 +40,10 @@ from esphome.const import ( PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, + PLATFORM_LN882X, PLATFORM_RP2040, PLATFORM_RTL87XX, + PlatformFramework, ) from esphome.core import CORE, Lambda, coroutine_with_priority @@ -100,6 +107,7 @@ UART_SELECTION_ESP8266 = [UART0, UART0_SWAP, UART1] UART_SELECTION_LIBRETINY = { COMPONENT_BK72XX: [DEFAULT, UART1, UART2], + COMPONENT_LN882X: [DEFAULT, UART0, UART1, UART2], COMPONENT_RTL87XX: [DEFAULT, UART0, UART1, UART2], } @@ -184,7 +192,9 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(Logger), cv.Optional(CONF_BAUD_RATE, default=115200): cv.positive_int, - cv.Optional(CONF_TX_BUFFER_SIZE, default=512): cv.validate_bytes, + cv.Optional(CONF_TX_BUFFER_SIZE, default=512): cv.All( + cv.validate_bytes, cv.int_range(min=160, max=65535) + ), cv.Optional(CONF_DEASSERT_RTS_DTR, default=False): cv.boolean, cv.SplitDefault( CONF_TASK_LOG_BUFFER_SIZE, @@ -215,6 +225,7 @@ CONFIG_SCHEMA = cv.All( esp32_p4_idf=USB_SERIAL_JTAG, rp2040=USB_CDC, bk72xx=DEFAULT, + ln882x=DEFAULT, rtl87xx=DEFAULT, ): cv.All( cv.only_on( @@ -223,6 +234,7 @@ CONFIG_SCHEMA = cv.All( PLATFORM_ESP32, PLATFORM_RP2040, PLATFORM_BK72XX, + PLATFORM_LN882X, PLATFORM_RTL87XX, ] ), @@ -324,7 +336,10 @@ async def to_code(config): if CORE.using_arduino: if config[CONF_HARDWARE_UART] == USB_CDC: cg.add_build_flag("-DARDUINO_USB_CDC_ON_BOOT=1") - if CORE.is_esp32 and get_esp32_variant() == VARIANT_ESP32C3: + if CORE.is_esp32 and get_esp32_variant() in ( + VARIANT_ESP32C3, + VARIANT_ESP32C6, + ): cg.add_build_flag("-DARDUINO_USB_MODE=1") if CORE.using_esp_idf: @@ -431,3 +446,25 @@ async def logger_set_level_to_code(config, action_id, template_arg, args): lambda_ = await cg.process_lambda(Lambda(text), args, return_type=cg.void) return cg.new_Pvariable(action_id, template_arg, lambda_) + + +FILTER_SOURCE_FILES = filter_source_files_from_platform( + { + "logger_esp32.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + }, + "logger_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, + "logger_host.cpp": {PlatformFramework.HOST_NATIVE}, + "logger_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO}, + "logger_libretiny.cpp": { + PlatformFramework.BK72XX_ARDUINO, + PlatformFramework.RTL87XX_ARDUINO, + PlatformFramework.LN882X_ARDUINO, + }, + "task_log_buffer.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + }, + } +) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 28a66b23b7..db807f7e53 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -24,7 +24,7 @@ static const char *const TAG = "logger"; // - Messages are serialized through main loop for proper console output // - Fallback to emergency console logging only if ring buffer is full // - WITHOUT task log buffer: Only emergency console output, no callbacks -void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char *format, va_list args) { // NOLINT +void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const char *format, va_list args) { // NOLINT if (level > this->level_for(tag)) return; @@ -46,8 +46,13 @@ void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char * bool message_sent = false; #ifdef USE_ESPHOME_TASK_LOG_BUFFER // For non-main tasks, queue the message for callbacks - but only if we have any callbacks registered - message_sent = this->log_buffer_->send_message_thread_safe(static_cast(level), tag, - static_cast(line), current_task, format, args); + message_sent = + this->log_buffer_->send_message_thread_safe(level, tag, static_cast(line), current_task, format, args); + if (message_sent) { + // Enable logger loop to process the buffered message + // This is safe to call from any context including ISRs + this->enable_loop_soon_any_context(); + } #endif // USE_ESPHOME_TASK_LOG_BUFFER // Emergency console logging for non-main tasks when ring buffer is full or disabled @@ -58,7 +63,7 @@ void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char * // Maximum size for console log messages (includes null terminator) static const size_t MAX_CONSOLE_LOG_MSG_SIZE = 144; char console_buffer[MAX_CONSOLE_LOG_MSG_SIZE]; // MUST be stack allocated for thread safety - int buffer_at = 0; // Initialize buffer position + uint16_t buffer_at = 0; // Initialize buffer position this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, console_buffer, &buffer_at, MAX_CONSOLE_LOG_MSG_SIZE); this->write_msg_(console_buffer); @@ -69,7 +74,7 @@ void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char * } #else // Implementation for all other platforms -void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char *format, va_list args) { // NOLINT +void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const char *format, va_list args) { // NOLINT if (level > this->level_for(tag) || global_recursion_guard_) return; @@ -85,7 +90,26 @@ void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char * #ifdef USE_STORE_LOG_STR_IN_FLASH // Implementation for ESP8266 with flash string support. // Note: USE_STORE_LOG_STR_IN_FLASH is only defined for ESP8266. -void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format, +// +// This function handles format strings stored in flash memory (PROGMEM) to save RAM. +// The buffer is used in a special way to avoid allocating extra memory: +// +// Memory layout during execution: +// Step 1: Copy format string from flash to buffer +// tx_buffer_: [format_string][null][.....................] +// tx_buffer_at_: ------------------^ +// msg_start: saved here -----------^ +// +// Step 2: format_log_to_buffer_with_terminator_ reads format string from beginning +// and writes formatted output starting at msg_start position +// tx_buffer_: [format_string][null][formatted_message][null] +// tx_buffer_at_: -------------------------------------^ +// +// Step 3: Output the formatted message (starting at msg_start) +// write_msg_ and callbacks receive: this->tx_buffer_ + msg_start +// which points to: [formatted_message][null] +// +void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __FlashStringHelper *format, va_list args) { // NOLINT if (level > this->level_for(tag) || global_recursion_guard_) return; @@ -116,13 +140,15 @@ void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStr if (this->baud_rate_ > 0) { this->write_msg_(this->tx_buffer_ + msg_start); } - this->log_callback_.call(level, tag, this->tx_buffer_ + msg_start); + size_t msg_length = + this->tx_buffer_at_ - msg_start; // Don't subtract 1 - tx_buffer_at_ is already at the null terminator position + this->log_callback_.call(level, tag, this->tx_buffer_ + msg_start, msg_length); global_recursion_guard_ = false; } #endif // USE_STORE_LOG_STR_IN_FLASH -inline int Logger::level_for(const char *tag) { +inline uint8_t Logger::level_for(const char *tag) { auto it = this->log_levels_.find(tag); if (it != this->log_levels_.end()) return it->second; @@ -139,6 +165,10 @@ Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate #ifdef USE_ESPHOME_TASK_LOG_BUFFER void Logger::init_log_buffer(size_t total_buffer_size) { this->log_buffer_ = esphome::make_unique(total_buffer_size); + + // Start with loop disabled when using task buffer (unless using USB CDC) + // The loop will be enabled automatically when messages arrive + this->disable_loop_when_buffer_empty_(); } #endif @@ -176,7 +206,8 @@ void Logger::loop() { this->tx_buffer_size_); this->write_footer_to_buffer_(this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_); this->tx_buffer_[this->tx_buffer_at_] = '\0'; - this->log_callback_.call(message->level, message->tag, this->tx_buffer_); + size_t msg_len = this->tx_buffer_at_; // We already know the length from tx_buffer_at_ + this->log_callback_.call(message->level, message->tag, this->tx_buffer_, msg_len); // At this point all the data we need from message has been transferred to the tx_buffer // so we can release the message to allow other tasks to use it as soon as possible. this->log_buffer_->release_message_main_loop(received_token); @@ -189,19 +220,23 @@ void Logger::loop() { this->write_msg_(this->tx_buffer_); } } + } else { + // No messages to process, disable loop if appropriate + // This reduces overhead when there's no async logging activity + this->disable_loop_when_buffer_empty_(); } #endif } #endif void Logger::set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; } -void Logger::set_log_level(const std::string &tag, int log_level) { this->log_levels_[tag] = log_level; } +void Logger::set_log_level(const std::string &tag, uint8_t log_level) { this->log_levels_[tag] = log_level; } #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) UARTSelection Logger::get_uart() const { return this->uart_; } #endif -void Logger::add_on_log_callback(std::function &&callback) { +void Logger::add_on_log_callback(std::function &&callback) { this->log_callback_.add(std::move(callback)); } float Logger::get_setup_priority() const { return setup_priority::BUS + 500.0f; } @@ -230,7 +265,7 @@ void Logger::dump_config() { } } -void Logger::set_log_level(int level) { +void Logger::set_log_level(uint8_t level) { if (level > ESPHOME_LOG_LEVEL) { level = ESPHOME_LOG_LEVEL; ESP_LOGW(TAG, "Cannot set log level higher than pre-compiled %s", LOG_LEVELS[ESPHOME_LOG_LEVEL]); diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 9f09208b66..fb68e75a51 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -61,7 +61,7 @@ static const char *const LOG_LEVEL_LETTERS[] = { * * Advanced configuration (pin selection, etc) is not supported. */ -enum UARTSelection { +enum UARTSelection : uint8_t { #ifdef USE_LIBRETINY UART_SELECTION_DEFAULT = 0, UART_SELECTION_UART0, @@ -129,10 +129,10 @@ class Logger : public Component { #endif /// Set the default log level for this logger. - void set_log_level(int level); + void set_log_level(uint8_t level); /// Set the log level of the specified tag. - void set_log_level(const std::string &tag, int log_level); - int get_log_level() { return this->current_level_; } + void set_log_level(const std::string &tag, uint8_t log_level); + uint8_t get_log_level() { return this->current_level_; } // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) @@ -140,19 +140,20 @@ class Logger : public Component { void pre_setup(); void dump_config() override; - inline int level_for(const char *tag); + inline uint8_t level_for(const char *tag); /// Register a callback that will be called for every log message sent - void add_on_log_callback(std::function &&callback); + void add_on_log_callback(std::function &&callback); // add a listener for log level changes - void add_listener(std::function &&callback) { this->level_callback_.add(std::move(callback)); } + void add_listener(std::function &&callback) { this->level_callback_.add(std::move(callback)); } float get_setup_priority() const override; - void log_vprintf_(int level, const char *tag, int line, const char *format, va_list args); // NOLINT + void log_vprintf_(uint8_t level, const char *tag, int line, const char *format, va_list args); // NOLINT #ifdef USE_STORE_LOG_STR_IN_FLASH - void log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format, va_list args); // NOLINT + void log_vprintf_(uint8_t level, const char *tag, int line, const __FlashStringHelper *format, + va_list args); // NOLINT #endif protected: @@ -160,8 +161,9 @@ class Logger : public Component { // Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator // It's the caller's responsibility to initialize buffer_at (typically to 0) - inline void HOT format_log_to_buffer_with_terminator_(int level, const char *tag, int line, const char *format, - va_list args, char *buffer, int *buffer_at, int buffer_size) { + inline void HOT format_log_to_buffer_with_terminator_(uint8_t level, const char *tag, int line, const char *format, + va_list args, char *buffer, uint16_t *buffer_at, + uint16_t buffer_size) { #if defined(USE_ESP32) || defined(USE_LIBRETINY) this->write_header_to_buffer_(level, tag, line, this->get_thread_name_(), buffer, buffer_at, buffer_size); #else @@ -180,7 +182,7 @@ class Logger : public Component { } // Helper to format and send a log message to both console and callbacks - inline void HOT log_message_to_buffer_and_send_(int level, const char *tag, int line, const char *format, + inline void HOT log_message_to_buffer_and_send_(uint8_t level, const char *tag, int line, const char *format, va_list args) { // Format to tx_buffer and prepare for output this->tx_buffer_at_ = 0; // Initialize buffer position @@ -190,15 +192,16 @@ class Logger : public Component { if (this->baud_rate_ > 0) { this->write_msg_(this->tx_buffer_); // If logging is enabled, write to console } - this->log_callback_.call(level, tag, this->tx_buffer_); + this->log_callback_.call(level, tag, this->tx_buffer_, this->tx_buffer_at_); } // Write the body of the log message to the buffer - inline void write_body_to_buffer_(const char *value, size_t length, char *buffer, int *buffer_at, int buffer_size) { + inline void write_body_to_buffer_(const char *value, size_t length, char *buffer, uint16_t *buffer_at, + uint16_t buffer_size) { // Calculate available space - const int available = buffer_size - *buffer_at; - if (available <= 0) + if (*buffer_at >= buffer_size) return; + const uint16_t available = buffer_size - *buffer_at; // Determine copy length (minimum of remaining capacity and string length) const size_t copy_len = (length < static_cast(available)) ? length : available; @@ -211,7 +214,7 @@ class Logger : public Component { } // Format string to explicit buffer with varargs - inline void printf_to_buffer_(char *buffer, int *buffer_at, int buffer_size, const char *format, ...) { + inline void printf_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size, const char *format, ...) { va_list arg; va_start(arg, format); this->format_body_to_buffer_(buffer, buffer_at, buffer_size, format, arg); @@ -222,41 +225,50 @@ class Logger : public Component { const char *get_uart_selection_(); #endif + // Group 4-byte aligned members first uint32_t baud_rate_; char *tx_buffer_{nullptr}; - int tx_buffer_at_{0}; - int tx_buffer_size_{0}; +#ifdef USE_ARDUINO + Stream *hw_serial_{nullptr}; +#endif +#if defined(USE_ESP32) || defined(USE_LIBRETINY) + void *main_task_ = nullptr; // Only used for thread name identification +#endif +#ifdef USE_ESP32 + // Task-specific recursion guards: + // - Main task uses a dedicated member variable for efficiency + // - Other tasks use pthread TLS with a dynamically created key via pthread_key_create + pthread_key_t log_recursion_key_; // 4 bytes +#endif +#ifdef USE_ESP_IDF + uart_port_t uart_num_; // 4 bytes (enum defaults to int size) +#endif + + // Large objects (internally aligned) + std::map log_levels_{}; + CallbackManager log_callback_{}; + CallbackManager level_callback_{}; +#ifdef USE_ESPHOME_TASK_LOG_BUFFER + std::unique_ptr log_buffer_; // Will be initialized with init_log_buffer +#endif + + // Group smaller types together at the end + uint16_t tx_buffer_at_{0}; + uint16_t tx_buffer_size_{0}; + uint8_t current_level_{ESPHOME_LOG_LEVEL_VERY_VERBOSE}; #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) UARTSelection uart_{UART_SELECTION_UART0}; #endif #ifdef USE_LIBRETINY UARTSelection uart_{UART_SELECTION_DEFAULT}; #endif -#ifdef USE_ARDUINO - Stream *hw_serial_{nullptr}; -#endif -#ifdef USE_ESP_IDF - uart_port_t uart_num_; -#endif - std::map log_levels_{}; - CallbackManager log_callback_{}; - int current_level_{ESPHOME_LOG_LEVEL_VERY_VERBOSE}; -#ifdef USE_ESPHOME_TASK_LOG_BUFFER - std::unique_ptr log_buffer_; // Will be initialized with init_log_buffer -#endif #ifdef USE_ESP32 - // Task-specific recursion guards: - // - Main task uses a dedicated member variable for efficiency - // - Other tasks use pthread TLS with a dynamically created key via pthread_key_create bool main_task_recursion_guard_{false}; - pthread_key_t log_recursion_key_; #else bool global_recursion_guard_{false}; // Simple global recursion guard for single-task platforms #endif - CallbackManager level_callback_{}; #if defined(USE_ESP32) || defined(USE_LIBRETINY) - void *main_task_ = nullptr; // Only used for thread name identification const char *HOT get_thread_name_() { TaskHandle_t current_task = xTaskGetCurrentTaskHandle(); if (current_task == main_task_) { @@ -297,11 +309,10 @@ class Logger : public Component { } #endif - inline void HOT write_header_to_buffer_(int level, const char *tag, int line, const char *thread_name, char *buffer, - int *buffer_at, int buffer_size) { + inline void HOT write_header_to_buffer_(uint8_t level, const char *tag, int line, const char *thread_name, + char *buffer, uint16_t *buffer_at, uint16_t buffer_size) { // Format header - if (level < 0) - level = 0; + // uint8_t level is already bounded 0-255, just ensure it's <= 7 if (level > 7) level = 7; @@ -320,12 +331,12 @@ class Logger : public Component { this->printf_to_buffer_(buffer, buffer_at, buffer_size, "%s[%s][%s:%03u]: ", color, letter, tag, line); } - inline void HOT format_body_to_buffer_(char *buffer, int *buffer_at, int buffer_size, const char *format, + inline void HOT format_body_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size, const char *format, va_list args) { // Get remaining capacity in the buffer - const int remaining = buffer_size - *buffer_at; - if (remaining <= 0) + if (*buffer_at >= buffer_size) return; + const uint16_t remaining = buffer_size - *buffer_at; const int ret = vsnprintf(buffer + *buffer_at, remaining, format, args); @@ -334,7 +345,7 @@ class Logger : public Component { } // Update buffer_at with the formatted length (handle truncation) - int formatted_len = (ret >= remaining) ? remaining : ret; + uint16_t formatted_len = (ret >= remaining) ? remaining : ret; *buffer_at += formatted_len; // Remove all trailing newlines right after formatting @@ -343,18 +354,38 @@ class Logger : public Component { } } - inline void HOT write_footer_to_buffer_(char *buffer, int *buffer_at, int buffer_size) { - static const int RESET_COLOR_LEN = strlen(ESPHOME_LOG_RESET_COLOR); + inline void HOT write_footer_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size) { + static constexpr uint16_t RESET_COLOR_LEN = sizeof(ESPHOME_LOG_RESET_COLOR) - 1; this->write_body_to_buffer_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN, buffer, buffer_at, buffer_size); } + +#ifdef USE_ESP32 + // Disable loop when task buffer is empty (with USB CDC check) + inline void disable_loop_when_buffer_empty_() { + // Thread safety note: This is safe even if another task calls enable_loop_soon_any_context() + // concurrently. If that happens between our check and disable_loop(), the enable request + // will be processed on the next main loop iteration since: + // - disable_loop() takes effect immediately + // - enable_loop_soon_any_context() sets a pending flag that's checked at loop start +#if defined(USE_LOGGER_USB_CDC) && defined(USE_ARDUINO) + // Only disable if not using USB CDC (which needs loop for connection detection) + if (this->uart_ != UART_SELECTION_USB_CDC) { + this->disable_loop(); + } +#else + // No USB CDC support, always safe to disable + this->disable_loop(); +#endif + } +#endif }; extern Logger *global_logger; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -class LoggerMessageTrigger : public Trigger { +class LoggerMessageTrigger : public Trigger { public: - explicit LoggerMessageTrigger(Logger *parent, int level) { + explicit LoggerMessageTrigger(Logger *parent, uint8_t level) { this->level_ = level; - parent->add_on_log_callback([this](int level, const char *tag, const char *message) { + parent->add_on_log_callback([this](uint8_t level, const char *tag, const char *message, size_t message_len) { if (level <= this->level_) { this->trigger(level, tag, message); } @@ -362,7 +393,7 @@ class LoggerMessageTrigger : public Trigger { } protected: - int level_; + uint8_t level_; }; } // namespace logger diff --git a/esphome/components/logger/logger_esp32.cpp b/esphome/components/logger/logger_esp32.cpp index b5ac84a665..2fde0f7d49 100644 --- a/esphome/components/logger/logger_esp32.cpp +++ b/esphome/components/logger/logger_esp32.cpp @@ -83,9 +83,7 @@ void init_uart(uart_port_t uart_num, uint32_t baud_rate, int tx_buffer_size) { uart_config.parity = UART_PARITY_DISABLE; uart_config.stop_bits = UART_STOP_BITS_1; uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE; -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) uart_config.source_clk = UART_SCLK_DEFAULT; -#endif uart_param_config(uart_num, &uart_config); const int uart_buffer_size = tx_buffer_size; // Install UART driver using an event queue here @@ -186,7 +184,9 @@ void HOT Logger::write_msg_(const char *msg) { ) { puts(msg); } else { - uart_write_bytes(this->uart_num_, msg, strlen(msg)); + // Use tx_buffer_at_ if msg points to tx_buffer_, otherwise fall back to strlen + size_t len = (msg == this->tx_buffer_) ? this->tx_buffer_at_ : strlen(msg); + uart_write_bytes(this->uart_num_, msg, len); uart_write_bytes(this->uart_num_, "\n", 1); } } diff --git a/esphome/components/lps22/__init__.py b/esphome/components/lps22/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/lps22/lps22.cpp b/esphome/components/lps22/lps22.cpp new file mode 100644 index 0000000000..526286ba72 --- /dev/null +++ b/esphome/components/lps22/lps22.cpp @@ -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(encode_uint16(t_buf[1], t_buf[0])); + float temp = TEMPERATURE_SCALE * static_cast(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(p_lsb)); + } + return RetryResult::DONE; +} + +} // namespace lps22 +} // namespace esphome diff --git a/esphome/components/lps22/lps22.h b/esphome/components/lps22/lps22.h new file mode 100644 index 0000000000..549ea524ea --- /dev/null +++ b/esphome/components/lps22/lps22.h @@ -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 diff --git a/esphome/components/lps22/sensor.py b/esphome/components/lps22/sensor.py new file mode 100644 index 0000000000..87a2106308 --- /dev/null +++ b/esphome/components/lps22/sensor.py @@ -0,0 +1,58 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_ID, + CONF_TEMPERATURE, + CONF_PRESSURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_HECTOPASCAL, + ICON_THERMOMETER, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_PRESSURE, +) + +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)) diff --git a/esphome/components/ltr390/ltr390.h b/esphome/components/ltr390/ltr390.h index 7359cbd336..7db73d68ff 100644 --- a/esphome/components/ltr390/ltr390.h +++ b/esphome/components/ltr390/ltr390.h @@ -44,7 +44,6 @@ enum LTR390RESOLUTION { class LTR390Component : public PollingComponent, public i2c::I2CDevice { public: - float get_setup_priority() const override { return setup_priority::DATA; } void setup() override; void dump_config() override; void update() override; diff --git a/esphome/components/ltr501/ltr501.h b/esphome/components/ltr501/ltr501.h index 07b69fa0d0..849ff6bc23 100644 --- a/esphome/components/ltr501/ltr501.h +++ b/esphome/components/ltr501/ltr501.h @@ -25,7 +25,6 @@ class LTRAlsPs501Component : public PollingComponent, public i2c::I2CDevice { // // EspHome framework functions // - float get_setup_priority() const override { return setup_priority::DATA; } void setup() override; void dump_config() override; void update() override; diff --git a/esphome/components/ltr_als_ps/ltr_als_ps.h b/esphome/components/ltr_als_ps/ltr_als_ps.h index 4cbbcea54c..2c768009ab 100644 --- a/esphome/components/ltr_als_ps/ltr_als_ps.h +++ b/esphome/components/ltr_als_ps/ltr_als_ps.h @@ -25,7 +25,6 @@ class LTRAlsPsComponent : public PollingComponent, public i2c::I2CDevice { // // EspHome framework functions // - float get_setup_priority() const override { return setup_priority::DATA; } void setup() override; void dump_config() override; void update() override; diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index dd49efd447..4a450375c4 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -466,7 +466,7 @@ LVGL_SCHEMA = cv.All( ): lvalid.lv_color, cv.Optional(df.CONF_THEME): cv.Schema( { - cv.Optional(name): obj_schema(w) + cv.Optional(name): obj_schema(w).extend(FULL_STYLE_SCHEMA) for name, w in WIDGET_TYPES.items() } ), diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index fdc8750d1d..959d203c41 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -21,7 +21,7 @@ from esphome.core.config import StartupTrigger from esphome.schema_extractors import SCHEMA_EXTRACT from . import defines as df, lv_validation as lvalid -from .defines import CONF_TIME_FORMAT, LV_GRAD_DIR +from .defines import CONF_TIME_FORMAT, LV_GRAD_DIR, TYPE_GRID from .helpers import add_lv_use, requires_component, validate_printf from .lv_validation import lv_color, lv_font, lv_gradient, lv_image, opacity from .lvcode import LvglComponent, lv_event_t_ptr @@ -349,7 +349,60 @@ def obj_schema(widget_type: WidgetType): ) +def _validate_grid_layout(config): + layout = config[df.CONF_LAYOUT] + rows = len(layout[df.CONF_GRID_ROWS]) + columns = len(layout[df.CONF_GRID_COLUMNS]) + used_cells = [[None] * columns for _ in range(rows)] + for index, widget in enumerate(config[df.CONF_WIDGETS]): + _, w = next(iter(widget.items())) + if (df.CONF_GRID_CELL_COLUMN_POS in w) != (df.CONF_GRID_CELL_ROW_POS in w): + # pylint: disable=raise-missing-from + raise cv.Invalid( + "Both row and column positions must be specified, or both omitted", + [df.CONF_WIDGETS, index], + ) + if df.CONF_GRID_CELL_ROW_POS in w: + row = w[df.CONF_GRID_CELL_ROW_POS] + column = w[df.CONF_GRID_CELL_COLUMN_POS] + else: + try: + row, column = next( + (r_idx, c_idx) + for r_idx, row in enumerate(used_cells) + for c_idx, value in enumerate(row) + if value is None + ) + except StopIteration: + # pylint: disable=raise-missing-from + raise cv.Invalid( + "No free cells available in grid layout", [df.CONF_WIDGETS, index] + ) + w[df.CONF_GRID_CELL_ROW_POS] = row + w[df.CONF_GRID_CELL_COLUMN_POS] = column + + for i in range(w[df.CONF_GRID_CELL_ROW_SPAN]): + for j in range(w[df.CONF_GRID_CELL_COLUMN_SPAN]): + if row + i >= rows or column + j >= columns: + # pylint: disable=raise-missing-from + raise cv.Invalid( + f"Cell at {row}/{column} span {w[df.CONF_GRID_CELL_ROW_SPAN]}x{w[df.CONF_GRID_CELL_COLUMN_SPAN]} " + f"exceeds grid size {rows}x{columns}", + [df.CONF_WIDGETS, index], + ) + if used_cells[row + i][column + j] is not None: + # pylint: disable=raise-missing-from + raise cv.Invalid( + f"Cell span {row + i}/{column + j} already occupied by widget at index {used_cells[row + i][column + j]}", + [df.CONF_WIDGETS, index], + ) + used_cells[row + i][column + j] = index + + return config + + LAYOUT_SCHEMAS = {} +LAYOUT_VALIDATORS = {TYPE_GRID: _validate_grid_layout} ALIGN_TO_SCHEMA = { cv.Optional(df.CONF_ALIGN_TO): cv.Schema( @@ -402,8 +455,8 @@ LAYOUT_SCHEMA = { } GRID_CELL_SCHEMA = { - cv.Required(df.CONF_GRID_CELL_ROW_POS): cv.positive_int, - cv.Required(df.CONF_GRID_CELL_COLUMN_POS): cv.positive_int, + cv.Optional(df.CONF_GRID_CELL_ROW_POS): cv.positive_int, + cv.Optional(df.CONF_GRID_CELL_COLUMN_POS): cv.positive_int, cv.Optional(df.CONF_GRID_CELL_ROW_SPAN, default=1): cv.positive_int, cv.Optional(df.CONF_GRID_CELL_COLUMN_SPAN, default=1): cv.positive_int, cv.Optional(df.CONF_GRID_CELL_X_ALIGN): grid_alignments, @@ -454,9 +507,13 @@ def container_validator(schema, widget_type: WidgetType): """ def validator(value): - result = schema if w_sch := widget_type.schema: - result = result.extend(w_sch) + if isinstance(w_sch, dict): + w_sch = cv.Schema(w_sch) + # order is important here to preserve extras + result = w_sch.extend(schema) + else: + result = schema ltype = df.TYPE_NONE if value and (layout := value.get(df.CONF_LAYOUT)): if not isinstance(layout, dict): @@ -470,7 +527,10 @@ def container_validator(schema, widget_type: WidgetType): result = result.extend( LAYOUT_SCHEMAS.get(ltype.lower(), LAYOUT_SCHEMAS[df.TYPE_NONE]) ) - return result(value) + value = result(value) + if layout_validator := LAYOUT_VALIDATORS.get(ltype): + value = layout_validator(value) + return value return validator diff --git a/esphome/components/lvgl/styles.py b/esphome/components/lvgl/styles.py index b59ff513e2..426dd3f229 100644 --- a/esphome/components/lvgl/styles.py +++ b/esphome/components/lvgl/styles.py @@ -3,7 +3,6 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_ID from esphome.core import ID -from esphome.cpp_generator import MockObj from .defines import ( CONF_STYLE_DEFINITIONS, @@ -13,12 +12,13 @@ from .defines import ( literal, ) from .helpers import add_lv_use -from .lvcode import LambdaContext, LocalVariable, lv, lv_assign, lv_variable +from .lvcode import LambdaContext, LocalVariable, lv from .schemas import ALL_STYLES, FULL_STYLE_SCHEMA, STYLE_REMAP -from .types import ObjUpdateAction, lv_lambda_t, lv_obj_t, lv_obj_t_ptr, lv_style_t +from .types import ObjUpdateAction, lv_obj_t, lv_style_t from .widgets import ( Widget, add_widgets, + collect_parts, set_obj_properties, theme_widget_map, wait_for_widgets, @@ -37,12 +37,18 @@ async def style_set(svar, style): lv.call(f"style_set_{remapped_prop}", svar, literal(value)) +async def create_style(style, id_name): + style_id = ID(id_name, True, lv_style_t) + svar = cg.new_Pvariable(style_id) + lv.style_init(svar) + await style_set(svar, style) + return svar + + async def styles_to_code(config): """Convert styles to C__ code.""" for style in config.get(CONF_STYLE_DEFINITIONS, ()): - svar = cg.new_Pvariable(style[CONF_ID]) - lv.style_init(svar) - await style_set(svar, style) + await create_style(style, style[CONF_ID].id) @automation.register_action( @@ -68,16 +74,18 @@ async def theme_to_code(config): if theme := config.get(CONF_THEME): add_lv_use(CONF_THEME) for w_name, style in theme.items(): - if not isinstance(style, dict): - continue - - lname = "lv_theme_apply_" + w_name - apply = lv_variable(lv_lambda_t, lname) - theme_widget_map[w_name] = apply - ow = Widget.create("obj", MockObj(ID("obj")), obj_spec) - async with LambdaContext([(lv_obj_t_ptr, "obj")], where=w_name) as context: - await set_obj_properties(ow, style) - lv_assign(apply, await context.get_lambda()) + # Work around Python 3.10 bug with nested async comprehensions + # With Python 3.11 this could be simplified + styles = {} + for part, states in collect_parts(style).items(): + styles[part] = { + state: await create_style( + props, + "_lv_theme_style_" + w_name + "_" + part + "_" + state, + ) + for state, props in states.items() + } + theme_widget_map[w_name] = styles async def add_top_layer(lv_component, config): diff --git a/esphome/components/lvgl/widgets/__init__.py b/esphome/components/lvgl/widgets/__init__.py index 9d53c0df26..a8cb8dce33 100644 --- a/esphome/components/lvgl/widgets/__init__.py +++ b/esphome/components/lvgl/widgets/__init__.py @@ -6,7 +6,7 @@ from esphome.config_validation import Invalid from esphome.const import CONF_DEFAULT, CONF_GROUP, CONF_ID, CONF_STATE, CONF_TYPE from esphome.core import ID, TimePeriod from esphome.coroutine import FakeAwaitable -from esphome.cpp_generator import CallExpression, MockObj +from esphome.cpp_generator import MockObj from ..defines import ( CONF_FLEX_ALIGN_CROSS, @@ -453,7 +453,17 @@ async def widget_to_code(w_cnfig, w_type: WidgetType, parent): w = Widget.create(wid, var, spec, w_cnfig) if theme := theme_widget_map.get(w_type): - lv_add(CallExpression(theme, w.obj)) + for part, states in theme.items(): + part = "LV_PART_" + part.upper() + for state, style in states.items(): + state = "LV_STATE_" + state.upper() + if state == "LV_STATE_DEFAULT": + lv_state = literal(part) + elif part == "LV_PART_MAIN": + lv_state = literal(state) + else: + lv_state = join_enums((state, part)) + lv.obj_add_style(w.obj, style, lv_state) await set_obj_properties(w, w_cnfig) await add_widgets(w, w_cnfig) await spec.to_code(w, w_cnfig) diff --git a/esphome/components/lvgl/widgets/lv_bar.py b/esphome/components/lvgl/widgets/lv_bar.py index 57209370c0..f0fdd6d278 100644 --- a/esphome/components/lvgl/widgets/lv_bar.py +++ b/esphome/components/lvgl/widgets/lv_bar.py @@ -1,8 +1,15 @@ import esphome.config_validation as cv from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_MODE, CONF_VALUE -from ..defines import BAR_MODES, CONF_ANIMATED, CONF_INDICATOR, CONF_MAIN, literal -from ..lv_validation import animated, get_start_value, lv_float +from ..defines import ( + BAR_MODES, + CONF_ANIMATED, + CONF_INDICATOR, + CONF_MAIN, + CONF_START_VALUE, + literal, +) +from ..lv_validation import animated, lv_int from ..lvcode import lv from ..types import LvNumber, NumberType from . import Widget @@ -10,22 +17,30 @@ from . import Widget # Note this file cannot be called "bar.py" because that name is disallowed. CONF_BAR = "bar" -BAR_MODIFY_SCHEMA = cv.Schema( - { - cv.Optional(CONF_VALUE): lv_float, - cv.Optional(CONF_ANIMATED, default=True): animated, - } -) + + +def validate_bar(config): + if config.get(CONF_MODE) != "LV_BAR_MODE_RANGE" and CONF_START_VALUE in config: + raise cv.Invalid( + f"{CONF_START_VALUE} is only allowed when {CONF_MODE} is set to 'RANGE'" + ) + if (CONF_MIN_VALUE in config) != (CONF_MAX_VALUE in config): + raise cv.Invalid( + f"If either {CONF_MIN_VALUE} or {CONF_MAX_VALUE} is set, both must be set" + ) + return config + BAR_SCHEMA = cv.Schema( { - cv.Optional(CONF_VALUE): lv_float, - cv.Optional(CONF_MIN_VALUE, default=0): cv.int_, - cv.Optional(CONF_MAX_VALUE, default=100): cv.int_, - cv.Optional(CONF_MODE, default="NORMAL"): BAR_MODES.one_of, + cv.Optional(CONF_VALUE): lv_int, + cv.Optional(CONF_START_VALUE): lv_int, + cv.Optional(CONF_MIN_VALUE): lv_int, + cv.Optional(CONF_MAX_VALUE): lv_int, + cv.Optional(CONF_MODE): BAR_MODES.one_of, cv.Optional(CONF_ANIMATED, default=True): animated, } -) +).add_extra(validate_bar) class BarType(NumberType): @@ -35,17 +50,23 @@ class BarType(NumberType): LvNumber("lv_bar_t"), parts=(CONF_MAIN, CONF_INDICATOR), schema=BAR_SCHEMA, - modify_schema=BAR_MODIFY_SCHEMA, ) async def to_code(self, w: Widget, config): var = w.obj + if mode := config.get(CONF_MODE): + lv.bar_set_mode(var, literal(mode)) + is_animated = literal(config[CONF_ANIMATED]) if CONF_MIN_VALUE in config: - lv.bar_set_range(var, config[CONF_MIN_VALUE], config[CONF_MAX_VALUE]) - lv.bar_set_mode(var, literal(config[CONF_MODE])) - value = await get_start_value(config) - if value is not None: - lv.bar_set_value(var, value, literal(config[CONF_ANIMATED])) + lv.bar_set_range( + var, + await lv_int.process(config[CONF_MIN_VALUE]), + await lv_int.process(config[CONF_MAX_VALUE]), + ) + if value := await lv_int.process(config.get(CONF_VALUE)): + lv.bar_set_value(var, value, is_animated) + if start_value := await lv_int.process(config.get(CONF_START_VALUE)): + lv.bar_set_start_value(var, start_value, is_animated) @property def animated(self): diff --git a/esphome/components/m5stack_8angle/light/m5stack_8angle_light.cpp b/esphome/components/m5stack_8angle/light/m5stack_8angle_light.cpp index 95fd8cb98f..0e7b902919 100644 --- a/esphome/components/m5stack_8angle/light/m5stack_8angle_light.cpp +++ b/esphome/components/m5stack_8angle/light/m5stack_8angle_light.cpp @@ -8,7 +8,7 @@ namespace m5stack_8angle { static const char *const TAG = "m5stack_8angle.light"; void M5Stack8AngleLightOutput::setup() { - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator allocator; this->buf_ = allocator.allocate(M5STACK_8ANGLE_NUM_LEDS * M5STACK_8ANGLE_BYTES_PER_LED); if (this->buf_ == nullptr) { ESP_LOGE(TAG, "Failed to allocate buffer of size %u", M5STACK_8ANGLE_NUM_LEDS * M5STACK_8ANGLE_BYTES_PER_LED); diff --git a/esphome/components/max9611/max9611.h b/esphome/components/max9611/max9611.h index 017f56b1a7..1eb7542aee 100644 --- a/esphome/components/max9611/max9611.h +++ b/esphome/components/max9611/max9611.h @@ -38,7 +38,6 @@ class MAX9611Component : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void update() override; void set_voltage_sensor(sensor::Sensor *vs) { voltage_sensor_ = vs; } void set_current_sensor(sensor::Sensor *cs) { current_sensor_ = cs; } diff --git a/esphome/components/mcp23016/__init__.py b/esphome/components/mcp23016/__init__.py index e15c643349..3333e46c97 100644 --- a/esphome/components/mcp23016/__init__.py +++ b/esphome/components/mcp23016/__init__.py @@ -50,7 +50,7 @@ MCP23016_PIN_SCHEMA = pins.gpio_base_schema( cv.int_range(min=0, max=15), modes=[CONF_INPUT, CONF_OUTPUT], mode_validator=validate_mode, - invertable=True, + invertible=True, ).extend( { cv.Required(CONF_MCP23016): cv.use_id(MCP23016), diff --git a/esphome/components/mcp23xxx_base/__init__.py b/esphome/components/mcp23xxx_base/__init__.py index c0e44d72de..8cf0ebcd44 100644 --- a/esphome/components/mcp23xxx_base/__init__.py +++ b/esphome/components/mcp23xxx_base/__init__.py @@ -60,7 +60,7 @@ MCP23XXX_PIN_SCHEMA = pins.gpio_base_schema( cv.int_range(min=0, max=15), modes=[CONF_INPUT, CONF_OUTPUT, CONF_PULLUP], mode_validator=validate_mode, - invertable=True, + invertible=True, ).extend( { cv.Required(CONF_MCP23XXX): cv.use_id(MCP23XXXBase), diff --git a/esphome/components/mcp4461/__init__.py b/esphome/components/mcp4461/__init__.py index 1764629ff3..f3ef6f4917 100644 --- a/esphome/components/mcp4461/__init__.py +++ b/esphome/components/mcp4461/__init__.py @@ -1,6 +1,6 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import i2c +import esphome.config_validation as cv from esphome.const import CONF_ID CODEOWNERS = ["@p1ngb4ck"] diff --git a/esphome/components/mcp4461/output/__init__.py b/esphome/components/mcp4461/output/__init__.py index ba59f97643..02bdbefed5 100644 --- a/esphome/components/mcp4461/output/__init__.py +++ b/esphome/components/mcp4461/output/__init__.py @@ -1,8 +1,9 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import output +import esphome.config_validation as cv from esphome.const import CONF_CHANNEL, CONF_ID, CONF_INITIAL_VALUE -from .. import Mcp4461Component, CONF_MCP4461_ID, mcp4461_ns + +from .. import CONF_MCP4461_ID, Mcp4461Component, mcp4461_ns DEPENDENCIES = ["mcp4461"] diff --git a/esphome/components/mcp9600/mcp9600.h b/esphome/components/mcp9600/mcp9600.h index 92612cc26d..c414653ea6 100644 --- a/esphome/components/mcp9600/mcp9600.h +++ b/esphome/components/mcp9600/mcp9600.h @@ -24,8 +24,6 @@ class MCP9600Component : public PollingComponent, public i2c::I2CDevice { void dump_config() override; void update() override; - float get_setup_priority() const override { return setup_priority::DATA; } - void set_hot_junction(sensor::Sensor *hot_junction) { this->hot_junction_sensor_ = hot_junction; } void set_cold_junction(sensor::Sensor *cold_junction) { this->cold_junction_sensor_ = cold_junction; } void set_thermocouple_type(MCP9600ThermocoupleType thermocouple_type) { diff --git a/esphome/components/md5/md5.cpp b/esphome/components/md5/md5.cpp index 31f52634be..980cb98699 100644 --- a/esphome/components/md5/md5.cpp +++ b/esphome/components/md5/md5.cpp @@ -7,7 +7,7 @@ namespace esphome { namespace md5 { -#if defined(USE_ARDUINO) && !defined(USE_RP2040) +#if defined(USE_ARDUINO) && !defined(USE_RP2040) && !defined(USE_ESP32) void MD5Digest::init() { memset(this->digest_, 0, 16); MD5Init(&this->ctx_); @@ -18,7 +18,7 @@ void MD5Digest::add(const uint8_t *data, size_t len) { MD5Update(&this->ctx_, da void MD5Digest::calculate() { MD5Final(this->digest_, &this->ctx_); } #endif // USE_ARDUINO && !USE_RP2040 -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 void MD5Digest::init() { memset(this->digest_, 0, 16); esp_rom_md5_init(&this->ctx_); @@ -27,7 +27,7 @@ void MD5Digest::init() { void MD5Digest::add(const uint8_t *data, size_t len) { esp_rom_md5_update(&this->ctx_, data, len); } void MD5Digest::calculate() { esp_rom_md5_final(this->digest_, &this->ctx_); } -#endif // USE_ESP_IDF +#endif // USE_ESP32 #ifdef USE_RP2040 void MD5Digest::init() { diff --git a/esphome/components/md5/md5.h b/esphome/components/md5/md5.h index cb6accf46f..be1df40423 100644 --- a/esphome/components/md5/md5.h +++ b/esphome/components/md5/md5.h @@ -3,16 +3,11 @@ #include "esphome/core/defines.h" #ifdef USE_MD5 -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include "esp_rom_md5.h" #define MD5_CTX_TYPE md5_context_t #endif -#if defined(USE_ARDUINO) && defined(USE_ESP32) -#include "rom/md5_hash.h" -#define MD5_CTX_TYPE MD5Context -#endif - #if defined(USE_ARDUINO) && defined(USE_ESP8266) #include #define MD5_CTX_TYPE md5_context_t diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index 4b5e40dfea..e32d39cede 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -1,5 +1,6 @@ import esphome.codegen as cg from esphome.components.esp32 import add_idf_component +from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( CONF_DISABLED, @@ -8,8 +9,7 @@ from esphome.const import ( CONF_PROTOCOL, CONF_SERVICE, CONF_SERVICES, - KEY_CORE, - KEY_FRAMEWORK_VERSION, + PlatformFramework, ) from esphome.core import CORE, coroutine_with_priority @@ -85,15 +85,8 @@ async def to_code(config): elif CORE.is_rp2040: cg.add_library("LEAmDNS", None) - if CORE.using_esp_idf and CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] >= cv.Version( - 5, 0, 0 - ): - add_idf_component( - name="mdns", - repo="https://github.com/espressif/esp-protocols.git", - ref="mdns-v1.8.2", - path="components/mdns", - ) + if CORE.using_esp_idf: + add_idf_component(name="espressif/mdns", ref="1.8.2") cg.add_define("USE_MDNS") @@ -117,3 +110,21 @@ async def to_code(config): ) cg.add(var.add_extra_service(exp)) + + +FILTER_SOURCE_FILES = filter_source_files_from_platform( + { + "mdns_esp32.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + }, + "mdns_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, + "mdns_host.cpp": {PlatformFramework.HOST_NATIVE}, + "mdns_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO}, + "mdns_libretiny.cpp": { + PlatformFramework.BK72XX_ARDUINO, + PlatformFramework.RTL87XX_ARDUINO, + PlatformFramework.LN882X_ARDUINO, + }, + } +) diff --git a/esphome/components/media_player/__init__.py b/esphome/components/media_player/__init__.py index ef76419de3..ccded1deb2 100644 --- a/esphome/components/media_player/__init__.py +++ b/esphome/components/media_player/__init__.py @@ -11,9 +11,9 @@ from esphome.const import ( CONF_VOLUME, ) from esphome.core import CORE +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.coroutine import coroutine_with_priority from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@jesserockz"] @@ -81,7 +81,7 @@ IsAnnouncingCondition = media_player_ns.class_( async def setup_media_player_core_(var, config): - await setup_entity(var, config) + await setup_entity(var, config, "media_player") for conf in config.get(CONF_ON_STATE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) @@ -143,6 +143,8 @@ _MEDIA_PLAYER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( } ) +_MEDIA_PLAYER_SCHEMA.add_extra(entity_duplicate_validator("media_player")) + def media_player_schema( class_: MockObjClass, @@ -166,7 +168,6 @@ def media_player_schema( MEDIA_PLAYER_SCHEMA = media_player_schema(MediaPlayer) MEDIA_PLAYER_SCHEMA.add_extra(cv.deprecated_schema_constant("media_player")) - MEDIA_PLAYER_ACTION_SCHEMA = automation.maybe_simple_id( cv.Schema( { diff --git a/esphome/components/micro_wake_word/__init__.py b/esphome/components/micro_wake_word/__init__.py index 0efe2ac288..cde8752157 100644 --- a/esphome/components/micro_wake_word/__init__.py +++ b/esphome/components/micro_wake_word/__init__.py @@ -449,11 +449,7 @@ async def to_code(config): cg.add_define("USE_MICRO_WAKE_WORD") cg.add_define("USE_OTA_STATE_CALLBACK") - esp32.add_idf_component( - name="esp-tflite-micro", - repo="https://github.com/espressif/esp-tflite-micro", - ref="v1.3.3.1", - ) + esp32.add_idf_component(name="espressif/esp-tflite-micro", ref="1.3.3~1") cg.add_build_flag("-DTF_LITE_STATIC_MEMORY") cg.add_build_flag("-DTF_LITE_DISABLE_X86_NEON") diff --git a/esphome/components/micro_wake_word/streaming_model.cpp b/esphome/components/micro_wake_word/streaming_model.cpp index 31341bba0d..2b073cce56 100644 --- a/esphome/components/micro_wake_word/streaming_model.cpp +++ b/esphome/components/micro_wake_word/streaming_model.cpp @@ -27,7 +27,7 @@ void VADModel::log_model_config() { } bool StreamingModel::load_model_() { - RAMAllocator arena_allocator(RAMAllocator::ALLOW_FAILURE); + RAMAllocator arena_allocator; if (this->tensor_arena_ == nullptr) { this->tensor_arena_ = arena_allocator.allocate(this->tensor_arena_size_); @@ -96,7 +96,7 @@ bool StreamingModel::load_model_() { void StreamingModel::unload_model() { this->interpreter_.reset(); - RAMAllocator arena_allocator(RAMAllocator::ALLOW_FAILURE); + RAMAllocator arena_allocator; if (this->tensor_arena_ != nullptr) { arena_allocator.deallocate(this->tensor_arena_, this->tensor_arena_size_); diff --git a/esphome/components/microphone/microphone.cpp b/esphome/components/microphone/microphone.cpp index b1289f3791..0fbb393fd2 100644 --- a/esphome/components/microphone/microphone.cpp +++ b/esphome/components/microphone/microphone.cpp @@ -5,17 +5,15 @@ namespace microphone { void Microphone::add_data_callback(std::function &)> &&data_callback) { std::function &)> mute_handled_callback = - [this, data_callback](const std::vector &data) { data_callback(this->silence_audio_(data)); }; + [this, data_callback](const std::vector &data) { + if (this->mute_state_) { + data_callback(std::vector(data.size(), 0)); + } else { + data_callback(data); + }; + }; this->data_callbacks_.add(std::move(mute_handled_callback)); } -std::vector Microphone::silence_audio_(std::vector data) { - if (this->mute_state_) { - std::memset((void *) data.data(), 0, data.size()); - } - - return data; -} - } // namespace microphone } // namespace esphome diff --git a/esphome/components/microphone/microphone.h b/esphome/components/microphone/microphone.h index ea4e979e20..fcf9822458 100644 --- a/esphome/components/microphone/microphone.h +++ b/esphome/components/microphone/microphone.h @@ -33,8 +33,6 @@ class Microphone { audio::AudioStreamInfo get_audio_stream_info() { return this->audio_stream_info_; } protected: - std::vector silence_audio_(std::vector data); - State state_{STATE_STOPPED}; bool mute_state_{false}; diff --git a/esphome/components/mipi_spi/display.py b/esphome/components/mipi_spi/display.py index e9ed97a2a2..061257e859 100644 --- a/esphome/components/mipi_spi/display.py +++ b/esphome/components/mipi_spi/display.py @@ -472,3 +472,4 @@ async def to_code(config): cg.add(var.set_writer(lambda_)) await display.register_display(var, config) await spi.register_spi_device(var, config) + cg.add(var.set_write_only(True)) diff --git a/esphome/components/mmc5603/mmc5603.cpp b/esphome/components/mmc5603/mmc5603.cpp index 86b1b23c15..7f78f9592a 100644 --- a/esphome/components/mmc5603/mmc5603.cpp +++ b/esphome/components/mmc5603/mmc5603.cpp @@ -39,7 +39,7 @@ void MMC5603Component::setup() { return; } - if (id != MMC56X3_CHIP_ID) { + if (id != 0 && id != MMC56X3_CHIP_ID) { // ID is not reported correctly by all chips, 0 on some chips ESP_LOGCONFIG(TAG, "Chip Wrong"); this->error_code_ = ID_REGISTERS; this->mark_failed(); diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index c2efa93fae..6350f43ef6 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -90,15 +90,24 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { } else { // data starts at 2 and length is 4 for read registers commands - if (this->role == ModbusRole::SERVER && (function_code == 0x1 || function_code == 0x3 || function_code == 0x4)) { - data_offset = 2; - data_len = 4; - } - - // the response for write command mirrors the requests and data starts at offset 2 instead of 3 for read commands - if (function_code == 0x5 || function_code == 0x06 || function_code == 0xF || function_code == 0x10) { - data_offset = 2; - data_len = 4; + if (this->role == ModbusRole::SERVER) { + if (function_code == 0x1 || function_code == 0x3 || function_code == 0x4 || function_code == 0x6) { + data_offset = 2; + data_len = 4; + } else if (function_code == 0x10) { + if (at < 6) { + return true; + } + data_offset = 2; + // starting address (2 bytes) + quantity of registers (2 bytes) + byte count itself (1 byte) + actual byte count + data_len = 2 + 2 + 1 + raw[6]; + } + } else { + // the response for write command mirrors the requests and data starts at offset 2 instead of 3 for read commands + if (function_code == 0x5 || function_code == 0x06 || function_code == 0xF || function_code == 0x10) { + data_offset = 2; + data_len = 4; + } } // Error ( msb indicates error ) @@ -132,6 +141,7 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { bool found = false; for (auto *device : this->devices_) { if (device->address_ == address) { + found = true; // Is it an error response? if ((function_code & 0x80) == 0x80) { ESP_LOGD(TAG, "Modbus error function code: 0x%X exception: %d", function_code, raw[2]); @@ -141,13 +151,21 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { // Ignore modbus exception not related to a pending command ESP_LOGD(TAG, "Ignoring Modbus error - not expecting a response"); } - } else if (this->role == ModbusRole::SERVER && (function_code == 0x3 || function_code == 0x4)) { - device->on_modbus_read_registers(function_code, uint16_t(data[1]) | (uint16_t(data[0]) << 8), - uint16_t(data[3]) | (uint16_t(data[2]) << 8)); - } else { - device->on_modbus_data(data); + continue; } - found = true; + if (this->role == ModbusRole::SERVER) { + if (function_code == 0x3 || function_code == 0x4) { + device->on_modbus_read_registers(function_code, uint16_t(data[1]) | (uint16_t(data[0]) << 8), + uint16_t(data[3]) | (uint16_t(data[2]) << 8)); + continue; + } + if (function_code == 0x6 || function_code == 0x10) { + device->on_modbus_write_registers(function_code, data); + continue; + } + } + // fallthrough for other function codes + device->on_modbus_data(data); } } waiting_for_response = 0; diff --git a/esphome/components/modbus/modbus.h b/esphome/components/modbus/modbus.h index 4a78ed4aab..ec35612690 100644 --- a/esphome/components/modbus/modbus.h +++ b/esphome/components/modbus/modbus.h @@ -59,11 +59,20 @@ class ModbusDevice { virtual void on_modbus_data(const std::vector &data) = 0; virtual void on_modbus_error(uint8_t function_code, uint8_t exception_code) {} virtual void on_modbus_read_registers(uint8_t function_code, uint16_t start_address, uint16_t number_of_registers){}; + virtual void on_modbus_write_registers(uint8_t function_code, const std::vector &data){}; void send(uint8_t function, uint16_t start_address, uint16_t number_of_entities, uint8_t payload_len = 0, const uint8_t *payload = nullptr) { this->parent_->send(this->address_, function, start_address, number_of_entities, payload_len, payload); } void send_raw(const std::vector &payload) { this->parent_->send_raw(payload); } + void send_error(uint8_t function_code, uint8_t exception_code) { + std::vector error_response; + error_response.reserve(3); + error_response.push_back(this->address_); + error_response.push_back(function_code | 0x80); + error_response.push_back(exception_code); + this->send_raw(error_response); + } // If more than one device is connected block sending a new command before a response is received bool waiting_for_response() { return parent_->waiting_for_response != 0; } diff --git a/esphome/components/modbus_controller/__init__.py b/esphome/components/modbus_controller/__init__.py index 61b60498d0..5ab82f5e17 100644 --- a/esphome/components/modbus_controller/__init__.py +++ b/esphome/components/modbus_controller/__init__.py @@ -39,6 +39,7 @@ CODEOWNERS = ["@martgras"] AUTO_LOAD = ["modbus"] CONF_READ_LAMBDA = "read_lambda" +CONF_WRITE_LAMBDA = "write_lambda" CONF_SERVER_REGISTERS = "server_registers" MULTI_CONF = True @@ -112,6 +113,22 @@ TYPE_REGISTER_MAP = { "FP32_R": 2, } +CPP_TYPE_REGISTER_MAP = { + "RAW": cg.uint16, + "U_WORD": cg.uint16, + "S_WORD": cg.int16, + "U_DWORD": cg.uint32, + "U_DWORD_R": cg.uint32, + "S_DWORD": cg.int32, + "S_DWORD_R": cg.int32, + "U_QWORD": cg.uint64, + "U_QWORD_R": cg.uint64, + "S_QWORD": cg.int64, + "S_QWORD_R": cg.int64, + "FP32": cg.float_, + "FP32_R": cg.float_, +} + ModbusCommandSentTrigger = modbus_controller_ns.class_( "ModbusCommandSentTrigger", automation.Trigger.template(cg.int_, cg.int_) ) @@ -132,6 +149,7 @@ ModbusServerRegisterSchema = cv.Schema( cv.Required(CONF_ADDRESS): cv.positive_int, cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum(SENSOR_VALUE_TYPE), cv.Required(CONF_READ_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda, } ) @@ -285,21 +303,35 @@ async def to_code(config): cg.add(var.set_offline_skip_updates(config[CONF_OFFLINE_SKIP_UPDATES])) if CONF_SERVER_REGISTERS in config: for server_register in config[CONF_SERVER_REGISTERS]: + server_register_var = cg.new_Pvariable( + server_register[CONF_ID], + server_register[CONF_ADDRESS], + server_register[CONF_VALUE_TYPE], + TYPE_REGISTER_MAP[server_register[CONF_VALUE_TYPE]], + ) + cpp_type = CPP_TYPE_REGISTER_MAP[server_register[CONF_VALUE_TYPE]] cg.add( - var.add_server_register( - cg.new_Pvariable( - server_register[CONF_ID], - server_register[CONF_ADDRESS], - server_register[CONF_VALUE_TYPE], - TYPE_REGISTER_MAP[server_register[CONF_VALUE_TYPE]], + server_register_var.set_read_lambda( + cg.TemplateArguments(cpp_type), + await cg.process_lambda( + server_register[CONF_READ_LAMBDA], + [(cg.uint16, "address")], + return_type=cpp_type, + ), + ) + ) + if CONF_WRITE_LAMBDA in server_register: + cg.add( + server_register_var.set_write_lambda( + cg.TemplateArguments(cpp_type), await cg.process_lambda( - server_register[CONF_READ_LAMBDA], - [], - return_type=cg.float_, + server_register[CONF_WRITE_LAMBDA], + parameters=[(cg.uint16, "address"), (cpp_type, "x")], + return_type=cg.bool_, ), ) ) - ) + cg.add(var.add_server_register(server_register_var)) await register_modbus_device(var, config) for conf in config.get(CONF_ON_COMMAND_SENT, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) diff --git a/esphome/components/modbus_controller/modbus_controller.cpp b/esphome/components/modbus_controller/modbus_controller.cpp index 48ff868087..0f3ddf920d 100644 --- a/esphome/components/modbus_controller/modbus_controller.cpp +++ b/esphome/components/modbus_controller/modbus_controller.cpp @@ -117,12 +117,17 @@ void ModbusController::on_modbus_read_registers(uint8_t function_code, uint16_t bool found = false; for (auto *server_register : this->server_registers_) { if (server_register->address == current_address) { - float value = server_register->read_lambda(); + if (!server_register->read_lambda) { + break; + } + int64_t value = server_register->read_lambda(); + ESP_LOGD(TAG, "Matched register. Address: 0x%02X. Value type: %zu. Register count: %u. Value: %s.", + server_register->address, static_cast(server_register->value_type), + server_register->register_count, server_register->format_value(value).c_str()); - ESP_LOGD(TAG, "Matched register. Address: 0x%02X. Value type: %zu. Register count: %u. Value: %0.1f.", - server_register->address, static_cast(server_register->value_type), - server_register->register_count, value); - std::vector payload = float_to_payload(value, server_register->value_type); + std::vector payload; + payload.reserve(server_register->register_count * 2); + number_to_payload(payload, value, server_register->value_type); sixteen_bit_response.insert(sixteen_bit_response.end(), payload.cbegin(), payload.cend()); current_address += server_register->register_count; found = true; @@ -132,11 +137,7 @@ void ModbusController::on_modbus_read_registers(uint8_t function_code, uint16_t if (!found) { ESP_LOGW(TAG, "Could not match any register to address %02X. Sending exception response.", current_address); - std::vector error_response; - error_response.push_back(this->address_); - error_response.push_back(0x81); - error_response.push_back(0x02); - this->send_raw(error_response); + send_error(function_code, 0x02); return; } } @@ -151,6 +152,86 @@ void ModbusController::on_modbus_read_registers(uint8_t function_code, uint16_t this->send(function_code, start_address, number_of_registers, response.size(), response.data()); } +void ModbusController::on_modbus_write_registers(uint8_t function_code, const std::vector &data) { + uint16_t number_of_registers; + uint16_t payload_offset; + + if (function_code == 0x10) { + number_of_registers = uint16_t(data[3]) | (uint16_t(data[2]) << 8); + if (number_of_registers == 0 || number_of_registers > 0x7B) { + ESP_LOGW(TAG, "Invalid number of registers %d. Sending exception response.", number_of_registers); + send_error(function_code, 3); + return; + } + uint16_t payload_size = data[4]; + if (payload_size != number_of_registers * 2) { + ESP_LOGW(TAG, "Payload size of %d bytes is not 2 times the number of registers (%d). Sending exception response.", + payload_size, number_of_registers); + send_error(function_code, 3); + return; + } + payload_offset = 5; + } else if (function_code == 0x06) { + number_of_registers = 1; + payload_offset = 2; + } else { + ESP_LOGW(TAG, "Invalid function code 0x%X. Sending exception response.", function_code); + send_error(function_code, 1); + return; + } + + uint16_t start_address = uint16_t(data[1]) | (uint16_t(data[0]) << 8); + ESP_LOGD(TAG, + "Received write holding registers for device 0x%X. FC: 0x%X. Start address: 0x%X. Number of registers: " + "0x%X.", + this->address_, function_code, start_address, number_of_registers); + + auto for_each_register = [this, start_address, number_of_registers, payload_offset]( + const std::function &callback) -> bool { + uint16_t offset = payload_offset; + for (uint16_t current_address = start_address; current_address < start_address + number_of_registers;) { + bool ok = false; + for (auto *server_register : this->server_registers_) { + if (server_register->address == current_address) { + ok = callback(server_register, offset); + current_address += server_register->register_count; + offset += server_register->register_count * sizeof(uint16_t); + break; + } + } + + if (!ok) { + return false; + } + } + return true; + }; + + // check all registers are writable before writing to any of them: + if (!for_each_register([](ServerRegister *server_register, uint16_t offset) -> bool { + return server_register->write_lambda != nullptr; + })) { + send_error(function_code, 1); + return; + } + + // Actually write to the registers: + if (!for_each_register([&data](ServerRegister *server_register, uint16_t offset) { + int64_t number = payload_to_number(data, server_register->value_type, offset, 0xFFFFFFFF); + return server_register->write_lambda(number); + })) { + send_error(function_code, 4); + return; + } + + std::vector response; + response.reserve(6); + response.push_back(this->address_); + response.push_back(function_code); + response.insert(response.end(), data.begin(), data.begin() + 4); + this->send_raw(response); +} + SensorSet ModbusController::find_sensors_(ModbusRegisterType register_type, uint16_t start_address) const { auto reg_it = std::find_if( std::begin(this->register_ranges_), std::end(this->register_ranges_), diff --git a/esphome/components/modbus_controller/modbus_controller.h b/esphome/components/modbus_controller/modbus_controller.h index dfd52e44bc..a86ad1ccb5 100644 --- a/esphome/components/modbus_controller/modbus_controller.h +++ b/esphome/components/modbus_controller/modbus_controller.h @@ -63,6 +63,10 @@ enum class SensorValueType : uint8_t { FP32_R = 0xD }; +inline bool value_type_is_float(SensorValueType v) { + return v == SensorValueType::FP32 || v == SensorValueType::FP32_R; +} + inline ModbusFunctionCode modbus_register_read_function(ModbusRegisterType reg_type) { switch (reg_type) { case ModbusRegisterType::COIL: @@ -253,18 +257,66 @@ class SensorItem { }; class ServerRegister { + using ReadLambda = std::function; + using WriteLambda = std::function; + public: - ServerRegister(uint16_t address, SensorValueType value_type, uint8_t register_count, - std::function read_lambda) { + ServerRegister(uint16_t address, SensorValueType value_type, uint8_t register_count) { this->address = address; this->value_type = value_type; this->register_count = register_count; - this->read_lambda = std::move(read_lambda); } + + template void set_read_lambda(const std::function &&user_read_lambda) { + this->read_lambda = [this, user_read_lambda]() -> int64_t { + T user_value = user_read_lambda(this->address); + if constexpr (std::is_same_v) { + return bit_cast(user_value); + } else { + return static_cast(user_value); + } + }; + } + + template + void set_write_lambda(const std::function &&user_write_lambda) { + this->write_lambda = [this, user_write_lambda](int64_t number) { + if constexpr (std::is_same_v) { + float float_value = bit_cast(static_cast(number)); + return user_write_lambda(this->address, float_value); + } + return user_write_lambda(this->address, static_cast(number)); + }; + } + + // Formats a raw value into a string representation based on the value type for debugging + std::string format_value(int64_t value) const { + switch (this->value_type) { + case SensorValueType::U_WORD: + case SensorValueType::U_DWORD: + case SensorValueType::U_DWORD_R: + case SensorValueType::U_QWORD: + case SensorValueType::U_QWORD_R: + return std::to_string(static_cast(value)); + case SensorValueType::S_WORD: + case SensorValueType::S_DWORD: + case SensorValueType::S_DWORD_R: + case SensorValueType::S_QWORD: + case SensorValueType::S_QWORD_R: + return std::to_string(value); + case SensorValueType::FP32_R: + case SensorValueType::FP32: + return str_sprintf("%.1f", bit_cast(static_cast(value))); + default: + return std::to_string(value); + } + } + uint16_t address{0}; SensorValueType value_type{SensorValueType::RAW}; uint8_t register_count{0}; - std::function read_lambda; + ReadLambda read_lambda; + WriteLambda write_lambda; }; // ModbusController::create_register_ranges_ tries to optimize register range @@ -444,8 +496,10 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice { void on_modbus_data(const std::vector &data) override; /// called when a modbus error response was received void on_modbus_error(uint8_t function_code, uint8_t exception_code) override; - /// called when a modbus request (function code 3 or 4) was parsed without errors + /// called when a modbus request (function code 0x03 or 0x04) was parsed without errors void on_modbus_read_registers(uint8_t function_code, uint16_t start_address, uint16_t number_of_registers) final; + /// called when a modbus request (function code 0x06 or 0x10) was parsed without errors + void on_modbus_write_registers(uint8_t function_code, const std::vector &data) final; /// default delegate called by process_modbus_data when a response has retrieved from the incoming queue void on_register_data(ModbusRegisterType register_type, uint16_t start_address, const std::vector &data); /// default delegate called by process_modbus_data when a response for a write response has retrieved from the @@ -529,7 +583,7 @@ inline float payload_to_float(const std::vector &data, const SensorItem int64_t number = payload_to_number(data, item.sensor_value_type, item.offset, item.bitmask); float float_value; - if (item.sensor_value_type == SensorValueType::FP32 || item.sensor_value_type == SensorValueType::FP32_R) { + if (value_type_is_float(item.sensor_value_type)) { float_value = bit_cast(static_cast(number)); } else { float_value = static_cast(number); @@ -541,7 +595,7 @@ inline float payload_to_float(const std::vector &data, const SensorItem inline std::vector float_to_payload(float value, SensorValueType value_type) { int64_t val; - if (value_type == SensorValueType::FP32 || value_type == SensorValueType::FP32_R) { + if (value_type_is_float(value_type)) { val = bit_cast(value); } else { val = llroundf(value); diff --git a/esphome/components/mopeka_pro_check/mopeka_pro_check.h b/esphome/components/mopeka_pro_check/mopeka_pro_check.h index c58406ac18..4cbe8f2afe 100644 --- a/esphome/components/mopeka_pro_check/mopeka_pro_check.h +++ b/esphome/components/mopeka_pro_check/mopeka_pro_check.h @@ -34,7 +34,6 @@ class MopekaProCheck : public Component, public esp32_ble_tracker::ESPBTDeviceLi bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_min_signal_quality(SensorReadQuality min) { this->min_signal_quality_ = min; }; void set_level(sensor::Sensor *level) { level_ = level; }; diff --git a/esphome/components/mopeka_std_check/mopeka_std_check.h b/esphome/components/mopeka_std_check/mopeka_std_check.h index 2a1d9d2dfc..b92445df34 100644 --- a/esphome/components/mopeka_std_check/mopeka_std_check.h +++ b/esphome/components/mopeka_std_check/mopeka_std_check.h @@ -48,7 +48,6 @@ class MopekaStdCheck : public Component, public esp32_ble_tracker::ESPBTDeviceLi bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_level(sensor::Sensor *level) { this->level_ = level; }; void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; }; diff --git a/esphome/components/mpl3115a2/mpl3115a2.h b/esphome/components/mpl3115a2/mpl3115a2.h index 00a6d90c52..05da71f830 100644 --- a/esphome/components/mpl3115a2/mpl3115a2.h +++ b/esphome/components/mpl3115a2/mpl3115a2.h @@ -91,8 +91,6 @@ class MPL3115A2Component : public PollingComponent, public i2c::I2CDevice { void dump_config() override; void update() override; - float get_setup_priority() const override { return setup_priority::DATA; } - protected: sensor::Sensor *temperature_{nullptr}; sensor::Sensor *altitude_{nullptr}; diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 63d8da5788..1a6fcabf42 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -5,6 +5,7 @@ from esphome.automation import Condition import esphome.codegen as cg from esphome.components import logger from esphome.components.esp32 import add_idf_sdkconfig_option +from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( CONF_AVAILABILITY, @@ -54,6 +55,7 @@ from esphome.const import ( PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, + PlatformFramework, ) from esphome.core import CORE, coroutine_with_priority @@ -68,6 +70,7 @@ def AUTO_LOAD(): CONF_DISCOVER_IP = "discover_ip" CONF_IDF_SEND_ASYNC = "idf_send_async" +CONF_WAIT_FOR_CONNECTION = "wait_for_connection" def validate_message_just_topic(value): @@ -298,6 +301,7 @@ CONFIG_SCHEMA = cv.All( } ), cv.Optional(CONF_PUBLISH_NAN_AS_NONE, default=False): cv.boolean, + cv.Optional(CONF_WAIT_FOR_CONNECTION, default=False): cv.boolean, } ), validate_config, @@ -453,6 +457,8 @@ async def to_code(config): cg.add(var.set_publish_nan_as_none(config[CONF_PUBLISH_NAN_AS_NONE])) + cg.add(var.set_wait_for_connection(config[CONF_WAIT_FOR_CONNECTION])) + MQTT_PUBLISH_ACTION_SCHEMA = cv.Schema( { @@ -592,3 +598,13 @@ async def mqtt_enable_to_code(config, action_id, template_arg, args): async def mqtt_disable_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) + + +FILTER_SOURCE_FILES = filter_source_files_from_platform( + { + "mqtt_backend_esp32.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + }, + } +) diff --git a/esphome/components/mqtt/mqtt_backend.h b/esphome/components/mqtt/mqtt_backend.h index 3962c40a42..0c1720ec34 100644 --- a/esphome/components/mqtt/mqtt_backend.h +++ b/esphome/components/mqtt/mqtt_backend.h @@ -17,7 +17,8 @@ enum class MQTTClientDisconnectReason : int8_t { MQTT_MALFORMED_CREDENTIALS = 4, MQTT_NOT_AUTHORIZED = 5, ESP8266_NOT_ENOUGH_SPACE = 6, - TLS_BAD_FINGERPRINT = 7 + TLS_BAD_FINGERPRINT = 7, + DNS_RESOLVE_ERROR = 8 }; /// internal struct for MQTT messages. diff --git a/esphome/components/mqtt/mqtt_backend_esp32.cpp b/esphome/components/mqtt/mqtt_backend_esp32.cpp index 64dc27d84b..a096408aa5 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.cpp +++ b/esphome/components/mqtt/mqtt_backend_esp32.cpp @@ -6,6 +6,7 @@ #include #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" namespace esphome { namespace mqtt { @@ -13,49 +14,6 @@ namespace mqtt { static const char *const TAG = "mqtt.idf"; bool MQTTBackendESP32::initialize_() { -#if ESP_IDF_VERSION_MAJOR < 5 - mqtt_cfg_.user_context = (void *) this; - mqtt_cfg_.buffer_size = MQTT_BUFFER_SIZE; - - mqtt_cfg_.host = this->host_.c_str(); - mqtt_cfg_.port = this->port_; - mqtt_cfg_.keepalive = this->keep_alive_; - mqtt_cfg_.disable_clean_session = !this->clean_session_; - - if (!this->username_.empty()) { - mqtt_cfg_.username = this->username_.c_str(); - if (!this->password_.empty()) { - mqtt_cfg_.password = this->password_.c_str(); - } - } - - if (!this->lwt_topic_.empty()) { - mqtt_cfg_.lwt_topic = this->lwt_topic_.c_str(); - this->mqtt_cfg_.lwt_qos = this->lwt_qos_; - this->mqtt_cfg_.lwt_retain = this->lwt_retain_; - - if (!this->lwt_message_.empty()) { - mqtt_cfg_.lwt_msg = this->lwt_message_.c_str(); - mqtt_cfg_.lwt_msg_len = this->lwt_message_.size(); - } - } - - if (!this->client_id_.empty()) { - mqtt_cfg_.client_id = this->client_id_.c_str(); - } - if (ca_certificate_.has_value()) { - mqtt_cfg_.cert_pem = ca_certificate_.value().c_str(); - mqtt_cfg_.skip_cert_common_name_check = skip_cert_cn_check_; - mqtt_cfg_.transport = MQTT_TRANSPORT_OVER_SSL; - - if (this->cl_certificate_.has_value() && this->cl_key_.has_value()) { - mqtt_cfg_.client_cert_pem = this->cl_certificate_.value().c_str(); - mqtt_cfg_.client_key_pem = this->cl_key_.value().c_str(); - } - } else { - mqtt_cfg_.transport = MQTT_TRANSPORT_OVER_TCP; - } -#else mqtt_cfg_.broker.address.hostname = this->host_.c_str(); mqtt_cfg_.broker.address.port = this->port_; mqtt_cfg_.session.keepalive = this->keep_alive_; @@ -94,15 +52,30 @@ bool MQTTBackendESP32::initialize_() { } else { mqtt_cfg_.broker.address.transport = MQTT_TRANSPORT_OVER_TCP; } -#endif + auto *mqtt_client = esp_mqtt_client_init(&mqtt_cfg_); if (mqtt_client) { handler_.reset(mqtt_client); is_initalized_ = true; esp_mqtt_client_register_event(mqtt_client, MQTT_EVENT_ANY, mqtt_event_handler, this); +#if defined(USE_MQTT_IDF_ENQUEUE) + // Create the task only after MQTT client is initialized successfully + // Use larger stack size when TLS is enabled + size_t stack_size = this->ca_certificate_.has_value() ? TASK_STACK_SIZE_TLS : TASK_STACK_SIZE; + xTaskCreate(esphome_mqtt_task, "esphome_mqtt", stack_size, (void *) this, TASK_PRIORITY, &this->task_handle_); + if (this->task_handle_ == nullptr) { + ESP_LOGE(TAG, "Failed to create MQTT task"); + // Clean up MQTT client since we can't start the async task + handler_.reset(); + is_initalized_ = false; + return false; + } + // Set the task handle so the queue can notify it + this->mqtt_queue_.set_task_to_notify(this->task_handle_); +#endif return true; } else { - ESP_LOGE(TAG, "Failed to initialize IDF-MQTT"); + ESP_LOGE(TAG, "Failed to init client"); return false; } } @@ -115,6 +88,26 @@ void MQTTBackendESP32::loop() { mqtt_event_handler_(event); mqtt_events_.pop(); } + +#if defined(USE_MQTT_IDF_ENQUEUE) + // Periodically log dropped messages to avoid blocking during spikes. + // During high load, many messages can be dropped in quick succession. + // Logging each drop immediately would flood the logs and potentially + // cause more drops if MQTT logging is enabled (cascade effect). + // Instead, we accumulate the count and log a summary periodically. + // IMPORTANT: Don't move this to the scheduler - if drops are due to memory + // pressure, the scheduler's heap allocations would make things worse. + uint32_t now = App.get_loop_component_start_time(); + // Handle rollover: (now - last_time) works correctly with unsigned arithmetic + // even when now < last_time due to rollover + if ((now - this->last_dropped_log_time_) >= DROP_LOG_INTERVAL_MS) { + uint16_t dropped = this->mqtt_queue_.get_and_reset_dropped_count(); + if (dropped > 0) { + ESP_LOGW(TAG, "Dropped %u messages (%us)", dropped, DROP_LOG_INTERVAL_MS / 1000); + } + this->last_dropped_log_time_ = now; + } +#endif } void MQTTBackendESP32::mqtt_event_handler_(const Event &event) { @@ -127,12 +120,20 @@ void MQTTBackendESP32::mqtt_event_handler_(const Event &event) { case MQTT_EVENT_CONNECTED: ESP_LOGV(TAG, "MQTT_EVENT_CONNECTED"); this->is_connected_ = true; +#if defined(USE_MQTT_IDF_ENQUEUE) + this->last_dropped_log_time_ = 0; + xTaskNotifyGive(this->task_handle_); +#endif this->on_connect_.call(event.session_present); break; case MQTT_EVENT_DISCONNECTED: ESP_LOGV(TAG, "MQTT_EVENT_DISCONNECTED"); // TODO is there a way to get the disconnect reason? this->is_connected_ = false; +#if defined(USE_MQTT_IDF_ENQUEUE) + this->last_dropped_log_time_ = 0; + xTaskNotifyGive(this->task_handle_); +#endif this->on_disconnect_.call(MQTTClientDisconnectReason::TCP_DISCONNECTED); break; @@ -188,6 +189,86 @@ void MQTTBackendESP32::mqtt_event_handler(void *handler_args, esp_event_base_t b } } +#if defined(USE_MQTT_IDF_ENQUEUE) +void MQTTBackendESP32::esphome_mqtt_task(void *params) { + MQTTBackendESP32 *this_mqtt = (MQTTBackendESP32 *) params; + + while (true) { + // Wait for notification indefinitely + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + + // Process all queued items + struct QueueElement *elem; + while ((elem = this_mqtt->mqtt_queue_.pop()) != nullptr) { + if (this_mqtt->is_connected_) { + switch (elem->type) { + case MQTT_QUEUE_TYPE_SUBSCRIBE: + esp_mqtt_client_subscribe(this_mqtt->handler_.get(), elem->topic, elem->qos); + break; + + case MQTT_QUEUE_TYPE_UNSUBSCRIBE: + esp_mqtt_client_unsubscribe(this_mqtt->handler_.get(), elem->topic); + break; + + case MQTT_QUEUE_TYPE_PUBLISH: + esp_mqtt_client_publish(this_mqtt->handler_.get(), elem->topic, elem->payload, elem->payload_len, elem->qos, + elem->retain); + break; + + default: + ESP_LOGE(TAG, "Invalid operation type from MQTT queue"); + break; + } + } + this_mqtt->mqtt_event_pool_.release(elem); + } + } + + // Clean up any remaining items in the queue + struct QueueElement *elem; + while ((elem = this_mqtt->mqtt_queue_.pop()) != nullptr) { + this_mqtt->mqtt_event_pool_.release(elem); + } + + // Note: EventPool destructor will clean up the pool itself + // Task will delete itself + vTaskDelete(nullptr); +} + +bool MQTTBackendESP32::enqueue_(MqttQueueTypeT type, const char *topic, int qos, bool retain, const char *payload, + size_t len) { + auto *elem = this->mqtt_event_pool_.allocate(); + + if (!elem) { + // Queue is full - increment counter but don't log immediately. + // Logging here can cause a cascade effect: if MQTT logging is enabled, + // each dropped message would generate a log message, which could itself + // be sent via MQTT, causing more drops and more logs in a feedback loop + // that eventually triggers a watchdog reset. Instead, we log periodically + // in loop() to prevent blocking the event loop during spikes. + this->mqtt_queue_.increment_dropped_count(); + return false; + } + + elem->type = type; + elem->qos = qos; + elem->retain = retain; + + // Use the helper to allocate and copy data + if (!elem->set_data(topic, payload, len)) { + // Allocation failed, return elem to pool + this->mqtt_event_pool_.release(elem); + // Increment counter without logging to avoid cascade effect during memory pressure + this->mqtt_queue_.increment_dropped_count(); + return false; + } + + // Push to queue - always succeeds since we allocated from the pool + this->mqtt_queue_.push(elem); + return true; +} +#endif // USE_MQTT_IDF_ENQUEUE + } // namespace mqtt } // namespace esphome #endif // USE_ESP32 diff --git a/esphome/components/mqtt/mqtt_backend_esp32.h b/esphome/components/mqtt/mqtt_backend_esp32.h index 9054702115..a24e75eaf9 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.h +++ b/esphome/components/mqtt/mqtt_backend_esp32.h @@ -6,9 +6,14 @@ #include #include +#include #include +#include +#include #include "esphome/components/network/ip_address.h" #include "esphome/core/helpers.h" +#include "esphome/core/lock_free_queue.h" +#include "esphome/core/event_pool.h" namespace esphome { namespace mqtt { @@ -42,9 +47,79 @@ struct Event { error_handle(*event.error_handle) {} }; +enum MqttQueueTypeT : uint8_t { + MQTT_QUEUE_TYPE_NONE = 0, + MQTT_QUEUE_TYPE_SUBSCRIBE, + MQTT_QUEUE_TYPE_UNSUBSCRIBE, + MQTT_QUEUE_TYPE_PUBLISH, +}; + +struct QueueElement { + char *topic; + char *payload; + uint16_t payload_len; // MQTT max payload is 64KiB + uint8_t type : 2; + uint8_t qos : 2; // QoS only needs values 0-2 + uint8_t retain : 1; + uint8_t reserved : 3; // Reserved for future use + + QueueElement() : topic(nullptr), payload(nullptr), payload_len(0), qos(0), retain(0), reserved(0) {} + + // Helper to set topic/payload (uses RAMAllocator) + bool set_data(const char *topic_str, const char *payload_data, size_t len) { + // Check payload size limit (MQTT max is 64KiB) + if (len > std::numeric_limits::max()) { + return false; + } + + // Use RAMAllocator with default flags (tries external RAM first, falls back to internal) + RAMAllocator allocator; + + // Allocate and copy topic + size_t topic_len = strlen(topic_str) + 1; + topic = allocator.allocate(topic_len); + if (!topic) + return false; + memcpy(topic, topic_str, topic_len); + + if (payload_data && len) { + payload = allocator.allocate(len); + if (!payload) { + allocator.deallocate(topic, topic_len); + topic = nullptr; + return false; + } + memcpy(payload, payload_data, len); + payload_len = static_cast(len); + } else { + payload = nullptr; + payload_len = 0; + } + return true; + } + + // Helper to release (uses RAMAllocator) + void release() { + RAMAllocator allocator; + if (topic) { + allocator.deallocate(topic, strlen(topic) + 1); + topic = nullptr; + } + if (payload) { + allocator.deallocate(payload, payload_len); + payload = nullptr; + } + payload_len = 0; + } +}; + class MQTTBackendESP32 final : public MQTTBackend { public: static const size_t MQTT_BUFFER_SIZE = 4096; + static const size_t TASK_STACK_SIZE = 3072; + static const size_t TASK_STACK_SIZE_TLS = 4096; // Larger stack for TLS operations + static const ssize_t TASK_PRIORITY = 5; + static const uint8_t MQTT_QUEUE_LENGTH = 30; // 30*12 bytes = 360 void set_keep_alive(uint16_t keep_alive) final { this->keep_alive_ = keep_alive; } void set_client_id(const char *client_id) final { this->client_id_ = client_id; } @@ -105,15 +180,23 @@ class MQTTBackendESP32 final : public MQTTBackend { } bool subscribe(const char *topic, uint8_t qos) final { +#if defined(USE_MQTT_IDF_ENQUEUE) + return enqueue_(MQTT_QUEUE_TYPE_SUBSCRIBE, topic, qos); +#else return esp_mqtt_client_subscribe(handler_.get(), topic, qos) != -1; +#endif + } + bool unsubscribe(const char *topic) final { +#if defined(USE_MQTT_IDF_ENQUEUE) + return enqueue_(MQTT_QUEUE_TYPE_UNSUBSCRIBE, topic); +#else + return esp_mqtt_client_unsubscribe(handler_.get(), topic) != -1; +#endif } - bool unsubscribe(const char *topic) final { return esp_mqtt_client_unsubscribe(handler_.get(), topic) != -1; } bool publish(const char *topic, const char *payload, size_t length, uint8_t qos, bool retain) final { #if defined(USE_MQTT_IDF_ENQUEUE) - // use the non-blocking version - // it can delay sending a couple of seconds but won't block - return esp_mqtt_client_enqueue(handler_.get(), topic, payload, length, qos, retain, true) != -1; + return enqueue_(MQTT_QUEUE_TYPE_PUBLISH, topic, qos, retain, payload, length); #else // might block for several seconds, either due to network timeout (10s) // or if publishing payloads longer than internal buffer (due to message fragmentation) @@ -129,6 +212,12 @@ class MQTTBackendESP32 final : public MQTTBackend { void set_cl_key(const std::string &key) { cl_key_ = key; } void set_skip_cert_cn_check(bool skip_check) { skip_cert_cn_check_ = skip_check; } + // No destructor needed: ESPHome components live for the entire device runtime. + // The MQTT task and queue will run until the device reboots or loses power, + // at which point the entire process terminates and FreeRTOS cleans up all tasks. + // Implementing a destructor would add complexity and potential race conditions + // for a scenario that never occurs in practice. + protected: bool initialize_(); void mqtt_event_handler_(const Event &event); @@ -160,6 +249,14 @@ class MQTTBackendESP32 final : public MQTTBackend { optional cl_certificate_; optional cl_key_; bool skip_cert_cn_check_{false}; +#if defined(USE_MQTT_IDF_ENQUEUE) + static void esphome_mqtt_task(void *params); + EventPool mqtt_event_pool_; + NotifyingLockFreeQueue mqtt_queue_; + TaskHandle_t task_handle_{nullptr}; + bool enqueue_(MqttQueueTypeT type, const char *topic, int qos = 0, bool retain = false, const char *payload = NULL, + size_t len = 0); +#endif // callbacks CallbackManager on_connect_; @@ -169,6 +266,11 @@ class MQTTBackendESP32 final : public MQTTBackend { CallbackManager on_message_; CallbackManager on_publish_; std::queue mqtt_events_; + +#if defined(USE_MQTT_IDF_ENQUEUE) + uint32_t last_dropped_log_time_{0}; + static constexpr uint32_t DROP_LOG_INTERVAL_MS = 10000; // Log every 10 seconds +#endif }; } // namespace mqtt diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index ceb56bdfbe..ab7fd15a35 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -57,14 +57,15 @@ void MQTTClientComponent::setup() { }); #ifdef USE_LOGGER if (this->is_log_message_enabled() && logger::global_logger != nullptr) { - logger::global_logger->add_on_log_callback([this](int level, const char *tag, const char *message) { - if (level <= this->log_level_ && this->is_connected()) { - this->publish({.topic = this->log_message_.topic, - .payload = message, - .qos = this->log_message_.qos, - .retain = this->log_message_.retain}); - } - }); + logger::global_logger->add_on_log_callback( + [this](int level, const char *tag, const char *message, size_t message_len) { + if (level <= this->log_level_ && this->is_connected()) { + this->publish({.topic = this->log_message_.topic, + .payload = std::string(message, message_len), + .qos = this->log_message_.qos, + .retain = this->log_message_.retain}); + } + }); } #endif @@ -176,7 +177,8 @@ void MQTTClientComponent::dump_config() { } } bool MQTTClientComponent::can_proceed() { - return network::is_disabled() || this->state_ == MQTT_CLIENT_DISABLED || this->is_connected(); + return network::is_disabled() || this->state_ == MQTT_CLIENT_DISABLED || this->is_connected() || + !this->wait_for_connection_; } void MQTTClientComponent::start_dnslookup_() { @@ -228,6 +230,8 @@ void MQTTClientComponent::check_dnslookup_() { if (this->dns_resolve_error_) { ESP_LOGW(TAG, "Couldn't resolve IP address for '%s'", this->credentials_.address.c_str()); this->state_ = MQTT_CLIENT_DISCONNECTED; + this->disconnect_reason_ = MQTTClientDisconnectReason::DNS_RESOLVE_ERROR; + this->on_disconnect_.call(MQTTClientDisconnectReason::DNS_RESOLVE_ERROR); return; } @@ -697,7 +701,9 @@ void MQTTClientComponent::set_on_connect(mqtt_on_connect_callback_t &&callback) } void MQTTClientComponent::set_on_disconnect(mqtt_on_disconnect_callback_t &&callback) { + auto callback_copy = callback; this->mqtt_backend_.set_on_disconnect(std::forward(callback)); + this->on_disconnect_.add(std::move(callback_copy)); } #if ASYNC_TCP_SSL_ENABLED diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index c68b3c62eb..325ca56f4b 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -4,11 +4,12 @@ #ifdef USE_MQTT -#include "esphome/core/component.h" -#include "esphome/core/automation.h" -#include "esphome/core/log.h" #include "esphome/components/json/json_util.h" #include "esphome/components/network/ip_address.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" #if defined(USE_ESP32) #include "mqtt_backend_esp32.h" #elif defined(USE_ESP8266) @@ -267,6 +268,8 @@ class MQTTClientComponent : public Component { void set_publish_nan_as_none(bool publish_nan_as_none); bool is_publish_nan_as_none() const; + void set_wait_for_connection(bool wait_for_connection) { this->wait_for_connection_ = wait_for_connection; } + protected: void send_device_info_(); @@ -332,8 +335,10 @@ class MQTTClientComponent : public Component { uint32_t connect_begin_; uint32_t last_connected_{0}; optional disconnect_reason_{}; + CallbackManager on_disconnect_; bool publish_nan_as_none_{false}; + bool wait_for_connection_{false}; }; extern MQTTClientComponent *global_mqtt_client; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/ms8607/ms8607.h b/esphome/components/ms8607/ms8607.h index 0bee7e97b7..67ce2817fa 100644 --- a/esphome/components/ms8607/ms8607.h +++ b/esphome/components/ms8607/ms8607.h @@ -37,7 +37,6 @@ class MS8607Component : public PollingComponent, public i2c::I2CDevice { void setup() override; void update() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; }; void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; } diff --git a/esphome/components/neopixelbus/light.py b/esphome/components/neopixelbus/light.py index affeb2de8f..3cd1bfd357 100644 --- a/esphome/components/neopixelbus/light.py +++ b/esphome/components/neopixelbus/light.py @@ -215,4 +215,7 @@ async def to_code(config): # https://github.com/Makuna/NeoPixelBus/blob/master/library.json # Version Listed Here: https://registry.platformio.org/libraries/makuna/NeoPixelBus/versions - cg.add_library("makuna/NeoPixelBus", "2.7.3") + if CORE.is_esp32: + cg.add_library("makuna/NeoPixelBus", "2.8.0") + else: + cg.add_library("makuna/NeoPixelBus", "2.7.3") diff --git a/esphome/components/neopixelbus/neopixelbus_light.h b/esphome/components/neopixelbus/neopixelbus_light.h index d94a923614..c27244b94d 100644 --- a/esphome/components/neopixelbus/neopixelbus_light.h +++ b/esphome/components/neopixelbus/neopixelbus_light.h @@ -1,6 +1,6 @@ #pragma once -#ifdef USE_ARDUINO +#if defined(USE_ARDUINO) && !defined(CLANG_TIDY) #include "esphome/core/color.h" #include "esphome/core/component.h" diff --git a/esphome/components/network/__init__.py b/esphome/components/network/__init__.py index 129b1ced06..b04fca7a1c 100644 --- a/esphome/components/network/__init__.py +++ b/esphome/components/network/__init__.py @@ -2,7 +2,7 @@ import esphome.codegen as cg from esphome.components.esp32 import add_idf_sdkconfig_option import esphome.config_validation as cv from esphome.const import CONF_ENABLE_IPV6, CONF_MIN_IPV6_ADDR_COUNT -from esphome.core import CORE +from esphome.core import CORE, coroutine_with_priority CODEOWNERS = ["@esphome/core"] AUTO_LOAD = ["mdns"] @@ -36,8 +36,11 @@ CONFIG_SCHEMA = cv.Schema( ) +@coroutine_with_priority(201.0) async def to_code(config): cg.add_define("USE_NETWORK") + if CORE.using_arduino and CORE.is_esp32: + cg.add_library("Networking", None) if (enable_ipv6 := config.get(CONF_ENABLE_IPV6, None)) is not None: cg.add_define("USE_NETWORK_IPV6", enable_ipv6) if enable_ipv6: diff --git a/esphome/components/network/ip_address.h b/esphome/components/network/ip_address.h index d76da573b5..5e6b0dbd96 100644 --- a/esphome/components/network/ip_address.h +++ b/esphome/components/network/ip_address.h @@ -56,6 +56,7 @@ struct IPAddress { IP_ADDR4(&ip_addr_, first, second, third, fourth); } IPAddress(const ip_addr_t *other_ip) { ip_addr_copy(ip_addr_, *other_ip); } + IPAddress(const char *in_address) { ipaddr_aton(in_address, &ip_addr_); } IPAddress(const std::string &in_address) { ipaddr_aton(in_address.c_str(), &ip_addr_); } IPAddress(ip4_addr_t *other_ip) { memcpy((void *) &ip_addr_, (void *) other_ip, sizeof(ip4_addr_t)); diff --git a/esphome/components/nextion/__init__.py b/esphome/components/nextion/__init__.py index fb75daf4ba..8adc49d68c 100644 --- a/esphome/components/nextion/__init__.py +++ b/esphome/components/nextion/__init__.py @@ -1,5 +1,7 @@ import esphome.codegen as cg from esphome.components import uart +from esphome.config_helpers import filter_source_files_from_platform +from esphome.const import PlatformFramework nextion_ns = cg.esphome_ns.namespace("nextion") Nextion = nextion_ns.class_("Nextion", cg.PollingComponent, uart.UARTDevice) @@ -8,3 +10,17 @@ nextion_ref = Nextion.operator("ref") CONF_NEXTION_ID = "nextion_id" CONF_PUBLISH_STATE = "publish_state" CONF_SEND_TO_NEXTION = "send_to_nextion" + +FILTER_SOURCE_FILES = filter_source_files_from_platform( + { + "nextion_upload_arduino.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP8266_ARDUINO, + PlatformFramework.RP2040_ARDUINO, + PlatformFramework.BK72XX_ARDUINO, + PlatformFramework.RTL87XX_ARDUINO, + PlatformFramework.LN882X_ARDUINO, + }, + "nextion_upload_idf.cpp": {PlatformFramework.ESP32_IDF}, + } +) diff --git a/esphome/components/nextion/base_component.py b/esphome/components/nextion/base_component.py index 98dea4b513..392481e39a 100644 --- a/esphome/components/nextion/base_component.py +++ b/esphome/components/nextion/base_component.py @@ -11,6 +11,7 @@ CONF_AUTO_WAKE_ON_TOUCH = "auto_wake_on_touch" CONF_BACKGROUND_PRESSED_COLOR = "background_pressed_color" CONF_COMMAND_SPACING = "command_spacing" CONF_COMPONENT_NAME = "component_name" +CONF_DUMP_DEVICE_INFO = "dump_device_info" CONF_EXIT_REPARSE_ON_START = "exit_reparse_on_start" CONF_FONT_ID = "font_id" CONF_FOREGROUND_PRESSED_COLOR = "foreground_pressed_color" diff --git a/esphome/components/nextion/binary_sensor/nextion_binarysensor.cpp b/esphome/components/nextion/binary_sensor/nextion_binarysensor.cpp index b6d4cc3f23..3628ac2f63 100644 --- a/esphome/components/nextion/binary_sensor/nextion_binarysensor.cpp +++ b/esphome/components/nextion/binary_sensor/nextion_binarysensor.cpp @@ -44,7 +44,7 @@ void NextionBinarySensor::set_state(bool state, bool publish, bool send_to_nexti return; if (send_to_nextion) { - if (this->nextion_->is_sleeping() || !this->visible_) { + if (this->nextion_->is_sleeping() || !this->component_flags_.visible) { this->needs_to_send_update_ = true; } else { this->needs_to_send_update_ = false; diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index 7f63ca147b..4254ae45fe 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -15,6 +15,7 @@ from . import Nextion, nextion_ns, nextion_ref from .base_component import ( CONF_AUTO_WAKE_ON_TOUCH, CONF_COMMAND_SPACING, + CONF_DUMP_DEVICE_INFO, CONF_EXIT_REPARSE_ON_START, CONF_MAX_COMMANDS_PER_LOOP, CONF_MAX_QUEUE_SIZE, @@ -57,6 +58,7 @@ CONFIG_SCHEMA = ( cv.positive_time_period_milliseconds, cv.Range(max=TimePeriod(milliseconds=255)), ), + cv.Optional(CONF_DUMP_DEVICE_INFO, default=False): cv.boolean, cv.Optional(CONF_EXIT_REPARSE_ON_START, default=False): cv.boolean, cv.Optional(CONF_MAX_COMMANDS_PER_LOOP): cv.uint16_t, cv.Optional(CONF_MAX_QUEUE_SIZE): cv.positive_int, @@ -95,7 +97,9 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_SKIP_CONNECTION_HANDSHAKE, default=False): cv.boolean, cv.Optional(CONF_START_UP_PAGE): cv.uint8_t, cv.Optional(CONF_TFT_URL): cv.url, - cv.Optional(CONF_TOUCH_SLEEP_TIMEOUT): cv.int_range(min=3, max=65535), + cv.Optional(CONF_TOUCH_SLEEP_TIMEOUT): cv.Any( + 0, cv.int_range(min=3, max=65535) + ), cv.Optional(CONF_WAKE_UP_PAGE): cv.uint8_t, } ) @@ -150,7 +154,7 @@ async def to_code(config): cg.add_define("USE_NEXTION_TFT_UPLOAD") cg.add(var.set_tft_url(config[CONF_TFT_URL])) if CORE.is_esp32 and CORE.using_arduino: - cg.add_library("WiFiClientSecure", None) + cg.add_library("NetworkClientSecure", None) cg.add_library("HTTPClient", None) elif CORE.is_esp32 and CORE.using_esp_idf: esp32.add_idf_sdkconfig_option("CONFIG_ESP_TLS_INSECURE", True) @@ -167,13 +171,19 @@ async def to_code(config): cg.add(var.set_wake_up_page(config[CONF_WAKE_UP_PAGE])) if CONF_START_UP_PAGE in config: + cg.add_define("USE_NEXTION_CONF_START_UP_PAGE") cg.add(var.set_start_up_page(config[CONF_START_UP_PAGE])) cg.add(var.set_auto_wake_on_touch(config[CONF_AUTO_WAKE_ON_TOUCH])) - cg.add(var.set_exit_reparse_on_start(config[CONF_EXIT_REPARSE_ON_START])) + if config[CONF_DUMP_DEVICE_INFO]: + cg.add_define("USE_NEXTION_CONFIG_DUMP_DEVICE_INFO") - cg.add(var.set_skip_connection_handshake(config[CONF_SKIP_CONNECTION_HANDSHAKE])) + if config[CONF_EXIT_REPARSE_ON_START]: + cg.add_define("USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START") + + if config[CONF_SKIP_CONNECTION_HANDSHAKE]: + cg.add_define("USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE") if max_commands_per_loop := config.get(CONF_MAX_COMMANDS_PER_LOOP): cg.add_define("USE_NEXTION_MAX_COMMANDS_PER_LOOP") diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 24c31713bc..66e2d26061 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -1,8 +1,8 @@ #include "nextion.h" -#include "esphome/core/util.h" -#include "esphome/core/log.h" -#include "esphome/core/application.h" #include +#include "esphome/core/application.h" +#include "esphome/core/log.h" +#include "esphome/core/util.h" namespace esphome { namespace nextion { @@ -11,28 +11,25 @@ static const char *const TAG = "nextion"; void Nextion::setup() { this->is_setup_ = false; - this->ignore_is_setup_ = true; + this->connection_state_.ignore_is_setup_ = true; - // Wake up the nextion - this->send_command_("bkcmd=0"); - this->send_command_("sleep=0"); + // Wake up the nextion and ensure clean communication state + this->send_command_("sleep=0"); // Exit sleep mode if sleeping + this->send_command_("bkcmd=0"); // Disable return data during init sequence - this->send_command_("bkcmd=0"); - this->send_command_("sleep=0"); - - // Reboot it + // Reset device for clean state - critical for reliable communication this->send_command_("rest"); - this->ignore_is_setup_ = false; + this->connection_state_.ignore_is_setup_ = false; } bool Nextion::send_command_(const std::string &command) { - if (!this->ignore_is_setup_ && !this->is_setup()) { + if (!this->connection_state_.ignore_is_setup_ && !this->is_setup()) { return false; } #ifdef USE_NEXTION_COMMAND_SPACING - if (!this->ignore_is_setup_ && !this->command_pacer_.can_send()) { + if (!this->connection_state_.ignore_is_setup_ && !this->command_pacer_.can_send()) { ESP_LOGN(TAG, "Command spacing: delaying command '%s'", command.c_str()); return false; } @@ -48,36 +45,31 @@ bool Nextion::send_command_(const std::string &command) { } bool Nextion::check_connect_() { - if (this->is_connected_) + if (this->connection_state_.is_connected_) return true; - // Check if the handshake should be skipped for the Nextion connection - if (this->skip_connection_handshake_) { - // Log the connection status without handshake - ESP_LOGW(TAG, "Connected (no handshake)"); - // Set the connection status to true - this->is_connected_ = true; - // Return true indicating the connection is set - return true; - } - +#ifdef USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE + ESP_LOGW(TAG, "Connected (no handshake)"); // Log the connection status without handshake + this->is_connected_ = true; // Set the connection status to true + return true; // Return true indicating the connection is set +#else // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE if (this->comok_sent_ == 0) { this->reset_(false); - this->ignore_is_setup_ = true; + this->connection_state_.ignore_is_setup_ = true; this->send_command_("boguscommand=0"); // bogus command. needed sometimes after updating - if (this->exit_reparse_on_start_) { - this->send_command_("DRAKJHSUYDGBNCJHGJKSHBDN"); - } +#ifdef USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START + this->send_command_("DRAKJHSUYDGBNCJHGJKSHBDN"); +#endif // USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START this->send_command_("connect"); - this->comok_sent_ = millis(); - this->ignore_is_setup_ = false; + this->comok_sent_ = App.get_loop_component_start_time(); + this->connection_state_.ignore_is_setup_ = false; return false; } - if (millis() - this->comok_sent_ <= 500) // Wait 500 ms + if (App.get_loop_component_start_time() - this->comok_sent_ <= 500) // Wait 500 ms return false; std::string response; @@ -94,16 +86,16 @@ bool Nextion::check_connect_() { for (size_t i = 0; i < response.length(); i++) { ESP_LOGN(TAG, "resp: %s %d %d %c", response.c_str(), i, response[i], response[i]); } -#endif +#endif // NEXTION_PROTOCOL_LOG ESP_LOGW(TAG, "Not connected"); comok_sent_ = 0; return false; } - this->ignore_is_setup_ = true; + this->connection_state_.ignore_is_setup_ = true; ESP_LOGI(TAG, "Connected"); - this->is_connected_ = true; + this->connection_state_.is_connected_ = true; ESP_LOGN(TAG, "connect: %s", response.c_str()); @@ -118,18 +110,27 @@ bool Nextion::check_connect_() { this->is_detected_ = (connect_info.size() == 7); if (this->is_detected_) { ESP_LOGN(TAG, "Connect info: %zu", connect_info.size()); - +#ifdef USE_NEXTION_CONFIG_DUMP_DEVICE_INFO this->device_model_ = connect_info[2]; this->firmware_version_ = connect_info[3]; this->serial_number_ = connect_info[5]; this->flash_size_ = connect_info[6]; +#else // USE_NEXTION_CONFIG_DUMP_DEVICE_INFO + ESP_LOGI(TAG, + " Device Model: %s\n" + " FW Version: %s\n" + " Serial Number: %s\n" + " Flash Size: %s\n", + connect_info[2].c_str(), connect_info[3].c_str(), connect_info[5].c_str(), connect_info[6].c_str()); +#endif // USE_NEXTION_CONFIG_DUMP_DEVICE_INFO } else { ESP_LOGE(TAG, "Bad connect value: '%s'", response.c_str()); } - this->ignore_is_setup_ = false; + this->connection_state_.ignore_is_setup_ = false; this->dump_config(); return true; +#endif // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE } void Nextion::reset_(bool reset_nextion) { @@ -144,36 +145,42 @@ void Nextion::reset_(bool reset_nextion) { void Nextion::dump_config() { ESP_LOGCONFIG(TAG, "Nextion:"); - if (this->skip_connection_handshake_) { - ESP_LOGCONFIG(TAG, " Skip handshake: %s", YESNO(this->skip_connection_handshake_)); - } else { - ESP_LOGCONFIG(TAG, - " Device Model: %s\n" - " FW Version: %s\n" - " Serial Number: %s\n" - " Flash Size: %s", - this->device_model_.c_str(), this->firmware_version_.c_str(), this->serial_number_.c_str(), - this->flash_size_.c_str()); - } + +#ifdef USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE + ESP_LOGCONFIG(TAG, " Skip handshake: YES"); +#else // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE ESP_LOGCONFIG(TAG, +#ifdef USE_NEXTION_CONFIG_DUMP_DEVICE_INFO + " Device Model: %s\n" + " FW Version: %s\n" + " Serial Number: %s\n" + " Flash Size: %s\n" +#endif // USE_NEXTION_CONFIG_DUMP_DEVICE_INFO +#ifdef USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START + " Exit reparse: YES\n" +#endif // USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START " Wake On Touch: %s\n" - " Exit reparse: %s", - YESNO(this->auto_wake_on_touch_), YESNO(this->exit_reparse_on_start_)); + " Touch Timeout: %" PRIu16, +#ifdef USE_NEXTION_CONFIG_DUMP_DEVICE_INFO + this->device_model_.c_str(), this->firmware_version_.c_str(), this->serial_number_.c_str(), + this->flash_size_.c_str(), +#endif // USE_NEXTION_CONFIG_DUMP_DEVICE_INFO + YESNO(this->connection_state_.auto_wake_on_touch_), this->touch_sleep_timeout_); +#endif // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE + #ifdef USE_NEXTION_MAX_COMMANDS_PER_LOOP ESP_LOGCONFIG(TAG, " Max commands per loop: %u", this->max_commands_per_loop_); #endif // USE_NEXTION_MAX_COMMANDS_PER_LOOP - if (this->touch_sleep_timeout_ != 0) { - ESP_LOGCONFIG(TAG, " Touch Timeout: %" PRIu32, this->touch_sleep_timeout_); + if (this->wake_up_page_ != 255) { + ESP_LOGCONFIG(TAG, " Wake Up Page: %u", this->wake_up_page_); } - if (this->wake_up_page_ != -1) { - ESP_LOGCONFIG(TAG, " Wake Up Page: %d", this->wake_up_page_); - } - - if (this->start_up_page_ != -1) { - ESP_LOGCONFIG(TAG, " Start Up Page: %d", this->start_up_page_); +#ifdef USE_NEXTION_CONF_START_UP_PAGE + if (this->start_up_page_ != 255) { + ESP_LOGCONFIG(TAG, " Start Up Page: %u", this->start_up_page_); } +#endif // USE_NEXTION_CONF_START_UP_PAGE #ifdef USE_NEXTION_COMMAND_SPACING ESP_LOGCONFIG(TAG, " Cmd spacing: %u ms", this->command_pacer_.get_spacing()); @@ -219,7 +226,7 @@ void Nextion::add_buffer_overflow_event_callback(std::function &&callbac } void Nextion::update_all_components() { - if ((!this->is_setup() && !this->ignore_is_setup_) || this->is_sleeping()) + if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_) || this->is_sleeping()) return; for (auto *binarysensortype : this->binarysensortype_) { @@ -237,7 +244,7 @@ void Nextion::update_all_components() { } bool Nextion::send_command(const char *command) { - if ((!this->is_setup() && !this->ignore_is_setup_) || this->is_sleeping()) + if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_) || this->is_sleeping()) return false; if (this->send_command_(command)) { @@ -248,7 +255,7 @@ bool Nextion::send_command(const char *command) { } bool Nextion::send_command_printf(const char *format, ...) { - if ((!this->is_setup() && !this->ignore_is_setup_) || this->is_sleeping()) + if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_) || this->is_sleeping()) return false; char buffer[256]; @@ -289,43 +296,72 @@ void Nextion::print_queue_members_() { #endif void Nextion::loop() { - if (!this->check_connect_() || this->is_updating_) + if (!this->check_connect_() || this->connection_state_.is_updating_) return; - if (this->nextion_reports_is_setup_ && !this->sent_setup_commands_) { - this->ignore_is_setup_ = true; - this->sent_setup_commands_ = true; + if (this->connection_state_.nextion_reports_is_setup_ && !this->connection_state_.sent_setup_commands_) { + this->connection_state_.ignore_is_setup_ = true; + this->connection_state_.sent_setup_commands_ = true; this->send_command_("bkcmd=3"); // Always, returns 0x00 to 0x23 result of serial command. if (this->brightness_.has_value()) { this->set_backlight_brightness(this->brightness_.value()); } +#ifdef USE_NEXTION_CONF_START_UP_PAGE // Check if a startup page has been set and send the command - if (this->start_up_page_ != -1) { + if (this->start_up_page_ != 255) { this->goto_page(this->start_up_page_); } +#endif // USE_NEXTION_CONF_START_UP_PAGE - if (this->wake_up_page_ != -1) { + if (this->wake_up_page_ != 255) { this->set_wake_up_page(this->wake_up_page_); } - this->ignore_is_setup_ = false; + if (this->touch_sleep_timeout_ != 0) { + this->set_touch_sleep_timeout(this->touch_sleep_timeout_); + } + + this->connection_state_.ignore_is_setup_ = false; } this->process_serial_(); // Receive serial data this->process_nextion_commands_(); // Process nextion return commands - if (!this->nextion_reports_is_setup_) { + if (!this->connection_state_.nextion_reports_is_setup_) { if (this->started_ms_ == 0) - this->started_ms_ = millis(); + this->started_ms_ = App.get_loop_component_start_time(); - if (this->started_ms_ + this->startup_override_ms_ < millis()) { + if (this->started_ms_ + this->startup_override_ms_ < App.get_loop_component_start_time()) { ESP_LOGD(TAG, "Manual ready set"); - this->nextion_reports_is_setup_ = true; + this->connection_state_.nextion_reports_is_setup_ = true; + } + } + +#ifdef USE_NEXTION_COMMAND_SPACING + // Try to send any pending commands if spacing allows + this->process_pending_in_queue_(); +#endif // USE_NEXTION_COMMAND_SPACING +} + +#ifdef USE_NEXTION_COMMAND_SPACING +void Nextion::process_pending_in_queue_() { + if (this->nextion_queue_.empty() || !this->command_pacer_.can_send()) { + return; + } + + // Check if first item in queue has a pending command + auto *front_item = this->nextion_queue_.front(); + if (front_item && !front_item->pending_command.empty()) { + if (this->send_command_(front_item->pending_command)) { + // Command sent successfully, clear the pending command + front_item->pending_command.clear(); + ESP_LOGVV(TAG, "Pending command sent: %s", front_item->component->get_variable_name().c_str()); } } } +#endif // USE_NEXTION_COMMAND_SPACING bool Nextion::remove_from_q_(bool report_empty) { if (this->nextion_queue_.empty()) { @@ -395,12 +431,12 @@ void Nextion::process_nextion_commands_() { ESP_LOGN(TAG, "Add 0xFF"); } - this->nextion_event_ = this->command_data_[0]; + const uint8_t nextion_event = this->command_data_[0]; to_process_length -= 1; to_process = this->command_data_.substr(1, to_process_length); - switch (this->nextion_event_) { + switch (nextion_event) { case 0x00: // instruction sent by user has failed ESP_LOGW(TAG, "Invalid instruction"); this->remove_from_q_(); @@ -409,7 +445,7 @@ void Nextion::process_nextion_commands_() { case 0x01: // instruction sent by user was successful ESP_LOGVV(TAG, "Cmd OK"); - ESP_LOGN(TAG, "this->nextion_queue_.empty() %s", this->nextion_queue_.empty() ? "True" : "False"); + ESP_LOGN(TAG, "this->nextion_queue_.empty() %s", YESNO(this->nextion_queue_.empty())); this->remove_from_q_(); if (!this->is_setup_) { @@ -421,7 +457,7 @@ void Nextion::process_nextion_commands_() { } #ifdef USE_NEXTION_COMMAND_SPACING this->command_pacer_.mark_sent(); // Here is where we should mark the command as sent - ESP_LOGN(TAG, "Command spacing: marked command sent at %u ms", millis()); + ESP_LOGN(TAG, "Command spacing: marked command sent"); #endif break; case 0x02: // invalid Component ID or name was used @@ -539,9 +575,9 @@ void Nextion::process_nextion_commands_() { break; } - uint16_t x = (uint16_t(to_process[0]) << 8) | to_process[1]; - uint16_t y = (uint16_t(to_process[2]) << 8) | to_process[3]; - uint8_t touch_event = to_process[4]; // 0 -> release, 1 -> press + const uint16_t x = (uint16_t(to_process[0]) << 8) | to_process[1]; + const uint16_t y = (uint16_t(to_process[2]) << 8) | to_process[3]; + const uint8_t touch_event = to_process[4]; // 0 -> release, 1 -> press ESP_LOGD(TAG, "Touch %s at %u,%u", touch_event ? "PRESS" : "RELEASE", x, y); break; } @@ -642,7 +678,7 @@ void Nextion::process_nextion_commands_() { case 0x88: // system successful start up { ESP_LOGD(TAG, "System start: %zu", to_process_length); - this->nextion_reports_is_setup_ = true; + this->connection_state_.nextion_reports_is_setup_ = true; break; } case 0x89: { // start SD card upgrade @@ -797,15 +833,14 @@ void Nextion::process_nextion_commands_() { break; } default: - ESP_LOGW(TAG, "Unknown event: 0x%02X", this->nextion_event_); + ESP_LOGW(TAG, "Unknown event: 0x%02X", nextion_event); break; } - // ESP_LOGN(TAG, "nextion_event_ deleting from 0 to %d", to_process_length + COMMAND_DELIMITER.length() + 1); this->command_data_.erase(0, to_process_length + COMMAND_DELIMITER.length() + 1); } - uint32_t ms = millis(); + const uint32_t ms = App.get_loop_component_start_time(); if (!this->nextion_queue_.empty() && this->nextion_queue_.front()->queue_time + this->max_q_age_ms_ < ms) { for (size_t i = 0; i < this->nextion_queue_.size(); i++) { @@ -937,14 +972,12 @@ void Nextion::update_components_by_prefix(const std::string &prefix) { } uint16_t Nextion::recv_ret_string_(std::string &response, uint32_t timeout, bool recv_flag) { - uint16_t ret = 0; uint8_t c = 0; uint8_t nr_of_ff_bytes = 0; - uint64_t start; bool exit_flag = false; bool ff_flag = false; - start = millis(); + const uint32_t start = millis(); while ((timeout == 0 && this->available()) || millis() - start <= timeout) { if (!this->available()) { @@ -981,8 +1014,7 @@ uint16_t Nextion::recv_ret_string_(std::string &response, uint32_t timeout, bool if (ff_flag) response = response.substr(0, response.length() - 3); // Remove last 3 0xFF - ret = response.length(); - return ret; + return response.length(); } /** @@ -1003,7 +1035,7 @@ void Nextion::add_no_result_to_queue_(const std::string &variable_name) { } #endif - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator allocator; nextion::NextionQueue *nextion_queue = allocator.allocate(1); if (nextion_queue == nullptr) { ESP_LOGW(TAG, "Queue alloc failed"); @@ -1029,17 +1061,50 @@ void Nextion::add_no_result_to_queue_(const std::string &variable_name) { * @param command */ void Nextion::add_no_result_to_queue_with_command_(const std::string &variable_name, const std::string &command) { - if ((!this->is_setup() && !this->ignore_is_setup_) || command.empty()) + if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_) || command.empty()) return; if (this->send_command_(command)) { this->add_no_result_to_queue_(variable_name); +#ifdef USE_NEXTION_COMMAND_SPACING + } else { + // Command blocked by spacing, add to queue WITH the command for retry + this->add_no_result_to_queue_with_pending_command_(variable_name, command); +#endif // USE_NEXTION_COMMAND_SPACING } } +#ifdef USE_NEXTION_COMMAND_SPACING +void Nextion::add_no_result_to_queue_with_pending_command_(const std::string &variable_name, + const std::string &command) { +#ifdef USE_NEXTION_MAX_QUEUE_SIZE + if (this->max_queue_size_ > 0 && this->nextion_queue_.size() >= this->max_queue_size_) { + ESP_LOGW(TAG, "Queue full (%zu), drop: %s", this->nextion_queue_.size(), variable_name.c_str()); + return; + } +#endif + + RAMAllocator allocator; + nextion::NextionQueue *nextion_queue = allocator.allocate(1); + if (nextion_queue == nullptr) { + ESP_LOGW(TAG, "Queue alloc failed"); + return; + } + new (nextion_queue) nextion::NextionQueue(); + + nextion_queue->component = new nextion::NextionComponentBase; + nextion_queue->component->set_variable_name(variable_name); + nextion_queue->queue_time = App.get_loop_component_start_time(); + nextion_queue->pending_command = command; // Store command for retry + + this->nextion_queue_.push_back(nextion_queue); + ESP_LOGVV(TAG, "Queue with pending command: %s", variable_name.c_str()); +} +#endif // USE_NEXTION_COMMAND_SPACING + bool Nextion::add_no_result_to_queue_with_ignore_sleep_printf_(const std::string &variable_name, const char *format, ...) { - if ((!this->is_setup() && !this->ignore_is_setup_)) + if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_)) return false; char buffer[256]; @@ -1064,7 +1129,7 @@ bool Nextion::add_no_result_to_queue_with_ignore_sleep_printf_(const std::string * @param ... The format arguments */ bool Nextion::add_no_result_to_queue_with_printf_(const std::string &variable_name, const char *format, ...) { - if ((!this->is_setup() && !this->ignore_is_setup_) || this->is_sleeping()) + if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_) || this->is_sleeping()) return false; char buffer[256]; @@ -1103,7 +1168,7 @@ void Nextion::add_no_result_to_queue_with_set(const std::string &variable_name, void Nextion::add_no_result_to_queue_with_set_internal_(const std::string &variable_name, const std::string &variable_name_to_send, int32_t state_value, bool is_sleep_safe) { - if ((!this->is_setup() && !this->ignore_is_setup_) || (!is_sleep_safe && this->is_sleeping())) + if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_) || (!is_sleep_safe && this->is_sleeping())) return; this->add_no_result_to_queue_with_ignore_sleep_printf_(variable_name, "%s=%" PRId32, variable_name_to_send.c_str(), @@ -1131,7 +1196,7 @@ void Nextion::add_no_result_to_queue_with_set(const std::string &variable_name, void Nextion::add_no_result_to_queue_with_set_internal_(const std::string &variable_name, const std::string &variable_name_to_send, const std::string &state_value, bool is_sleep_safe) { - if ((!this->is_setup() && !this->ignore_is_setup_) || (!is_sleep_safe && this->is_sleeping())) + if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_) || (!is_sleep_safe && this->is_sleeping())) return; this->add_no_result_to_queue_with_printf_(variable_name, "%s=\"%s\"", variable_name_to_send.c_str(), @@ -1148,7 +1213,7 @@ void Nextion::add_no_result_to_queue_with_set_internal_(const std::string &varia * @param component Pointer to the Nextion component that will handle the response. */ void Nextion::add_to_get_queue(NextionComponentBase *component) { - if ((!this->is_setup() && !this->ignore_is_setup_)) + if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_)) return; #ifdef USE_NEXTION_MAX_QUEUE_SIZE @@ -1159,7 +1224,7 @@ void Nextion::add_to_get_queue(NextionComponentBase *component) { } #endif - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator allocator; nextion::NextionQueue *nextion_queue = allocator.allocate(1); if (nextion_queue == nullptr) { ESP_LOGW(TAG, "Queue alloc failed"); @@ -1168,7 +1233,7 @@ void Nextion::add_to_get_queue(NextionComponentBase *component) { new (nextion_queue) nextion::NextionQueue(); nextion_queue->component = component; - nextion_queue->queue_time = millis(); + nextion_queue->queue_time = App.get_loop_component_start_time(); ESP_LOGN(TAG, "Queue %s: %s", component->get_queue_type_string().c_str(), component->get_variable_name().c_str()); @@ -1188,10 +1253,10 @@ void Nextion::add_to_get_queue(NextionComponentBase *component) { * @param buffer_size The buffer data */ void Nextion::add_addt_command_to_queue(NextionComponentBase *component) { - if ((!this->is_setup() && !this->ignore_is_setup_) || this->is_sleeping()) + if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_) || this->is_sleeping()) return; - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator allocator; nextion::NextionQueue *nextion_queue = allocator.allocate(1); if (nextion_queue == nullptr) { ESP_LOGW(TAG, "Queue alloc failed"); @@ -1200,7 +1265,7 @@ void Nextion::add_addt_command_to_queue(NextionComponentBase *component) { new (nextion_queue) nextion::NextionQueue(); nextion_queue->component = component; - nextion_queue->queue_time = millis(); + nextion_queue->queue_time = App.get_loop_component_start_time(); this->waveform_queue_.push_back(nextion_queue); if (this->waveform_queue_.size() == 1) @@ -1229,7 +1294,7 @@ void Nextion::set_writer(const nextion_writer_t &writer) { this->writer_ = write ESPDEPRECATED("set_wait_for_ack(bool) deprecated, no effect", "v1.20") void Nextion::set_wait_for_ack(bool wait_for_ack) { ESP_LOGE(TAG, "Deprecated"); } -bool Nextion::is_updating() { return this->is_updating_; } +bool Nextion::is_updating() { return this->connection_state_.is_updating_; } } // namespace nextion } // namespace esphome diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index 036fbe6c6d..e2c4faa1d0 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -932,21 +932,6 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe */ void set_backlight_brightness(float brightness); - /** - * Sets whether the Nextion display should skip the connection handshake process. - * @param skip_handshake True or false. When skip_connection_handshake is true, - * the connection will be established without performing the handshake. - * This can be useful when using Nextion Simulator. - * - * Example: - * ```cpp - * it.set_skip_connection_handshake(true); - * ``` - * - * When set to true, the display will be marked as connected without performing a handshake. - */ - void set_skip_connection_handshake(bool skip_handshake) { this->skip_connection_handshake_ = skip_handshake; } - /** * Sets Nextion mode between sleep and awake * @param True or false. Sleep=true to enter sleep mode or sleep=false to exit sleep mode. @@ -1179,22 +1164,43 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe void update_components_by_prefix(const std::string &prefix); /** - * Set the touch sleep timeout of the display. - * @param timeout Timeout in seconds. + * Set the touch sleep timeout of the display using the `thsp` command. + * + * Sets internal No-touch-then-sleep timer to specified value in seconds. + * Nextion will auto-enter sleep mode if and when this timer expires. + * + * @param touch_sleep_timeout Timeout in seconds. + * Range: 3 to 65535 seconds (minimum 3 seconds, maximum ~18 hours 12 minutes 15 seconds) + * Use 0 to disable touch sleep timeout. + * + * @note Once `thsp` is set, it will persist until reboot or reset. The Nextion device + * needs to exit sleep mode to issue `thsp=0` to disable sleep on no touch. + * + * @note The display will only wake up by a restart or by setting up `thup` (auto wake on touch). + * See set_auto_wake_on_touch() to configure wake behavior. * * Example: * ```cpp + * // Set 30 second touch timeout * it.set_touch_sleep_timeout(30); + * + * // Set maximum timeout (~18 hours) + * it.set_touch_sleep_timeout(65535); + * + * // Disable touch sleep timeout + * it.set_touch_sleep_timeout(0); * ``` * - * After 30 seconds the display will go to sleep. Note: the display will only wakeup by a restart or by setting up - * `thup`. + * Related Nextion instruction: `thsp=` + * + * @see set_auto_wake_on_touch() Configure automatic wake on touch + * @see sleep() Manually control sleep state */ - void set_touch_sleep_timeout(uint32_t touch_sleep_timeout); + void set_touch_sleep_timeout(uint16_t touch_sleep_timeout = 0); /** * Sets which page Nextion loads when exiting sleep mode. Note this can be set even when Nextion is in sleep mode. - * @param wake_up_page The page id, from 0 to the lage page in Nextion. Set 255 (not set to any existing page) to + * @param wake_up_page The page id, from 0 to the last page in Nextion. Set 255 (not set to any existing page) to * wakes up to current page. * * Example: @@ -1206,9 +1212,10 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe */ void set_wake_up_page(uint8_t wake_up_page = 255); +#ifdef USE_NEXTION_CONF_START_UP_PAGE /** * Sets which page Nextion loads when connecting to ESPHome. - * @param start_up_page The page id, from 0 to the lage page in Nextion. Set 255 (not set to any existing page) to + * @param start_up_page The page id, from 0 to the last page in Nextion. Set 255 (not set to any existing page) to * wakes up to current page. * * Example: @@ -1219,6 +1226,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * The display will go to page 2 when it establishes a connection to ESPHome. */ void set_start_up_page(uint8_t start_up_page = 255) { this->start_up_page_ = start_up_page; } +#endif // USE_NEXTION_CONF_START_UP_PAGE /** * Sets if Nextion should auto-wake from sleep when touch press occurs. @@ -1234,20 +1242,6 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe */ void set_auto_wake_on_touch(bool auto_wake_on_touch); - /** - * Sets if Nextion should exit the active reparse mode before the "connect" command is sent - * @param exit_reparse_on_start True or false. When exit_reparse_on_start is true, the exit reparse command - * will be sent before requesting the connection from Nextion. - * - * Example: - * ```cpp - * it.set_exit_reparse_on_start(true); - * ``` - * - * The display will be requested to leave active reparse mode before setup. - */ - void set_exit_reparse_on_start(bool exit_reparse_on_start) { this->exit_reparse_on_start_ = exit_reparse_on_start; } - /** * @brief Retrieves the number of commands pending in the Nextion command queue. * @@ -1290,7 +1284,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * the Nextion display. A connection is considered established when: * * - The initial handshake with the display is completed successfully, or - * - The handshake is skipped via skip_connection_handshake_ flag + * - The handshake is skipped via USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE flag * * The connection status is particularly useful when: * - Troubleshooting communication issues @@ -1300,7 +1294,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * @return true if the Nextion display is connected and ready to receive commands * @return false if the display is not yet connected or connection was lost */ - bool is_connected() { return this->is_connected_; } + bool is_connected() { return this->connection_state_.is_connected_; } protected: #ifdef USE_NEXTION_MAX_COMMANDS_PER_LOOP @@ -1309,34 +1303,54 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe #ifdef USE_NEXTION_MAX_QUEUE_SIZE size_t max_queue_size_{0}; #endif // USE_NEXTION_MAX_QUEUE_SIZE + #ifdef USE_NEXTION_COMMAND_SPACING NextionCommandPacer command_pacer_{0}; + + /** + * @brief Process any commands in the queue that are pending due to command spacing + * + * This method checks if the first item in the nextion_queue_ has a pending command + * that was previously blocked by command spacing. If spacing now allows and a + * pending command exists, it attempts to send the command. Once successfully sent, + * the pending command is cleared and the queue item continues normal processing. + * + * Called from loop() to retry sending commands that were delayed by spacing. + */ + void process_pending_in_queue_(); #endif // USE_NEXTION_COMMAND_SPACING + std::deque nextion_queue_; std::deque waveform_queue_; uint16_t recv_ret_string_(std::string &response, uint32_t timeout, bool recv_flag); void all_components_send_state_(bool force_update = false); - uint64_t comok_sent_ = 0; + uint32_t comok_sent_ = 0; bool remove_from_q_(bool report_empty = true); /** - * @brief - * Sends commands ignoring of the Nextion has been setup. + * @brief Status flags for Nextion display state management + * + * Uses bitfields to pack multiple boolean states into a single byte, + * saving 5 bytes of RAM compared to individual bool variables. */ - bool ignore_is_setup_ = false; - - bool nextion_reports_is_setup_ = false; - uint8_t nextion_event_; + struct { + uint8_t is_connected_ : 1; ///< Connection established with Nextion display + uint8_t sent_setup_commands_ : 1; ///< Initial setup commands have been sent + uint8_t ignore_is_setup_ : 1; ///< Temporarily ignore setup state for special operations + uint8_t nextion_reports_is_setup_ : 1; ///< Nextion has reported successful initialization + uint8_t is_updating_ : 1; ///< TFT firmware update is currently in progress + uint8_t auto_wake_on_touch_ : 1; ///< Display should wake automatically on touch (default: true) + uint8_t reserved_ : 2; ///< Reserved bits for future flag additions + } connection_state_{}; ///< Zero-initialized status flags (all start as false) void process_nextion_commands_(); void process_serial_(); - bool is_updating_ = false; - uint32_t touch_sleep_timeout_ = 0; - int16_t wake_up_page_ = -1; - int16_t start_up_page_ = -1; + uint16_t touch_sleep_timeout_ = 0; + uint8_t wake_up_page_ = 255; +#ifdef USE_NEXTION_CONF_START_UP_PAGE + uint8_t start_up_page_ = 255; +#endif // USE_NEXTION_CONF_START_UP_PAGE bool auto_wake_on_touch_ = true; - bool exit_reparse_on_start_ = false; - bool skip_connection_handshake_ = false; /** * Manually send a raw command to the display and don't wait for an acknowledgement packet. @@ -1348,6 +1362,23 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe __attribute__((format(printf, 3, 4))); void add_no_result_to_queue_with_command_(const std::string &variable_name, const std::string &command); +#ifdef USE_NEXTION_COMMAND_SPACING + /** + * @brief Add a command to the Nextion queue with a pending command for retry + * + * This method creates a queue entry for a command that was blocked by command spacing. + * The command string is stored in the queue item's pending_command field so it can + * be retried later when spacing allows. This ensures commands are not lost when + * sent too quickly. + * + * If the max_queue_size limit is configured and reached, the command will be dropped. + * + * @param variable_name Name of the variable or component associated with the command + * @param command The actual command string to be sent when spacing allows + */ + void add_no_result_to_queue_with_pending_command_(const std::string &variable_name, const std::string &command); +#endif // USE_NEXTION_COMMAND_SPACING + bool add_no_result_to_queue_with_printf_(const std::string &variable_name, const char *format, ...) __attribute__((format(printf, 3, 4))); @@ -1426,10 +1457,12 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe optional writer_; optional brightness_; +#ifdef USE_NEXTION_CONFIG_DUMP_DEVICE_INFO std::string device_model_; std::string firmware_version_; std::string serial_number_; std::string flash_size_; +#endif // USE_NEXTION_CONFIG_DUMP_DEVICE_INFO void remove_front_no_sensors_(); @@ -1439,11 +1472,9 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe void reset_(bool reset_nextion = true); std::string command_data_; - bool is_connected_ = false; const uint16_t startup_override_ms_ = 8000; const uint16_t max_q_age_ms_ = 8000; uint32_t started_ms_ = 0; - bool sent_setup_commands_ = false; }; } // namespace nextion diff --git a/esphome/components/nextion/nextion_commands.cpp b/esphome/components/nextion/nextion_commands.cpp index 0226e0a13c..f3a282717b 100644 --- a/esphome/components/nextion/nextion_commands.cpp +++ b/esphome/components/nextion/nextion_commands.cpp @@ -15,14 +15,15 @@ void Nextion::set_wake_up_page(uint8_t wake_up_page) { this->add_no_result_to_queue_with_set_internal_("wake_up_page", "wup", wake_up_page, true); } -void Nextion::set_touch_sleep_timeout(uint32_t touch_sleep_timeout) { - if (touch_sleep_timeout < 3) { - ESP_LOGD(TAG, "Sleep timeout out of bounds (3-65535)"); - return; +void Nextion::set_touch_sleep_timeout(const uint16_t touch_sleep_timeout) { + // Validate range: Nextion thsp command requires min 3, max 65535 seconds (0 disables) + if (touch_sleep_timeout != 0 && touch_sleep_timeout < 3) { + this->touch_sleep_timeout_ = 3; // Auto-correct to minimum valid value + } else { + this->touch_sleep_timeout_ = touch_sleep_timeout; } - this->touch_sleep_timeout_ = touch_sleep_timeout; - this->add_no_result_to_queue_with_set_internal_("touch_sleep_timeout", "thsp", touch_sleep_timeout, true); + this->add_no_result_to_queue_with_set_internal_("touch_sleep_timeout", "thsp", this->touch_sleep_timeout_, true); } void Nextion::sleep(bool sleep) { @@ -38,7 +39,7 @@ void Nextion::sleep(bool sleep) { // Protocol reparse mode bool Nextion::set_protocol_reparse_mode(bool active_mode) { ESP_LOGV(TAG, "Reparse mode: %s", YESNO(active_mode)); - this->ignore_is_setup_ = true; // if not in reparse mode setup will fail, so it should be ignored + this->connection_state_.ignore_is_setup_ = true; // if not in reparse mode setup will fail, so it should be ignored bool all_commands_sent = true; if (active_mode) { // Sets active protocol reparse mode all_commands_sent &= this->send_command_("recmod=1"); @@ -48,10 +49,10 @@ bool Nextion::set_protocol_reparse_mode(bool active_mode) { all_commands_sent &= this->send_command_("recmod=0"); // Sending recmode=0 twice is recommended all_commands_sent &= this->send_command_("recmod=0"); } - if (!this->nextion_reports_is_setup_) { // No need to connect if is already setup + if (!this->connection_state_.nextion_reports_is_setup_) { // No need to connect if is already setup all_commands_sent &= this->send_command_("connect"); } - this->ignore_is_setup_ = false; + this->connection_state_.ignore_is_setup_ = false; return all_commands_sent; } @@ -191,7 +192,7 @@ void Nextion::set_backlight_brightness(float brightness) { } void Nextion::set_auto_wake_on_touch(bool auto_wake_on_touch) { - this->auto_wake_on_touch_ = auto_wake_on_touch; + this->connection_state_.auto_wake_on_touch_ = auto_wake_on_touch; this->add_no_result_to_queue_with_set("auto_wake_on_touch", "thup", auto_wake_on_touch ? 1 : 0); } diff --git a/esphome/components/nextion/nextion_component.cpp b/esphome/components/nextion/nextion_component.cpp index cfb4e3600c..32929d6845 100644 --- a/esphome/components/nextion/nextion_component.cpp +++ b/esphome/components/nextion/nextion_component.cpp @@ -8,8 +8,8 @@ void NextionComponent::set_background_color(Color bco) { return; // This is a variable. no need to set color } this->bco_ = bco; - this->bco_needs_update_ = true; - this->bco_is_set_ = true; + this->component_flags_.bco_needs_update = true; + this->component_flags_.bco_is_set = true; this->update_component_settings(); } @@ -19,8 +19,8 @@ void NextionComponent::set_background_pressed_color(Color bco2) { } this->bco2_ = bco2; - this->bco2_needs_update_ = true; - this->bco2_is_set_ = true; + this->component_flags_.bco2_needs_update = true; + this->component_flags_.bco2_is_set = true; this->update_component_settings(); } @@ -29,8 +29,8 @@ void NextionComponent::set_foreground_color(Color pco) { return; // This is a variable. no need to set color } this->pco_ = pco; - this->pco_needs_update_ = true; - this->pco_is_set_ = true; + this->component_flags_.pco_needs_update = true; + this->component_flags_.pco_is_set = true; this->update_component_settings(); } @@ -39,8 +39,8 @@ void NextionComponent::set_foreground_pressed_color(Color pco2) { return; // This is a variable. no need to set color } this->pco2_ = pco2; - this->pco2_needs_update_ = true; - this->pco2_is_set_ = true; + this->component_flags_.pco2_needs_update = true; + this->component_flags_.pco2_is_set = true; this->update_component_settings(); } @@ -49,8 +49,8 @@ void NextionComponent::set_font_id(uint8_t font_id) { return; // This is a variable. no need to set color } this->font_id_ = font_id; - this->font_id_needs_update_ = true; - this->font_id_is_set_ = true; + this->component_flags_.font_id_needs_update = true; + this->component_flags_.font_id_is_set = true; this->update_component_settings(); } @@ -58,20 +58,20 @@ void NextionComponent::set_visible(bool visible) { if (this->variable_name_ == this->variable_name_to_send_) { return; // This is a variable. no need to set color } - this->visible_ = visible; - this->visible_needs_update_ = true; - this->visible_is_set_ = true; + this->component_flags_.visible = visible; + this->component_flags_.visible_needs_update = true; + this->component_flags_.visible_is_set = true; this->update_component_settings(); } void NextionComponent::update_component_settings(bool force_update) { - if (this->nextion_->is_sleeping() || !this->nextion_->is_setup() || !this->visible_is_set_ || - (!this->visible_needs_update_ && !this->visible_)) { + if (this->nextion_->is_sleeping() || !this->nextion_->is_setup() || !this->component_flags_.visible_is_set || + (!this->component_flags_.visible_needs_update && !this->component_flags_.visible)) { this->needs_to_send_update_ = true; return; } - if (this->visible_needs_update_ || (force_update && this->visible_is_set_)) { + if (this->component_flags_.visible_needs_update || (force_update && this->component_flags_.visible_is_set)) { std::string name_to_send = this->variable_name_; size_t pos = name_to_send.find_last_of('.'); @@ -79,9 +79,9 @@ void NextionComponent::update_component_settings(bool force_update) { name_to_send = name_to_send.substr(pos + 1); } - this->visible_needs_update_ = false; + this->component_flags_.visible_needs_update = false; - if (this->visible_) { + if (this->component_flags_.visible) { this->nextion_->show_component(name_to_send.c_str()); this->send_state_to_nextion(); } else { @@ -90,26 +90,26 @@ void NextionComponent::update_component_settings(bool force_update) { } } - if (this->bco_needs_update_ || (force_update && this->bco2_is_set_)) { + if (this->component_flags_.bco_needs_update || (force_update && this->component_flags_.bco2_is_set)) { this->nextion_->set_component_background_color(this->variable_name_.c_str(), this->bco_); - this->bco_needs_update_ = false; + this->component_flags_.bco_needs_update = false; } - if (this->bco2_needs_update_ || (force_update && this->bco2_is_set_)) { + if (this->component_flags_.bco2_needs_update || (force_update && this->component_flags_.bco2_is_set)) { this->nextion_->set_component_pressed_background_color(this->variable_name_.c_str(), this->bco2_); - this->bco2_needs_update_ = false; + this->component_flags_.bco2_needs_update = false; } - if (this->pco_needs_update_ || (force_update && this->pco_is_set_)) { + if (this->component_flags_.pco_needs_update || (force_update && this->component_flags_.pco_is_set)) { this->nextion_->set_component_foreground_color(this->variable_name_.c_str(), this->pco_); - this->pco_needs_update_ = false; + this->component_flags_.pco_needs_update = false; } - if (this->pco2_needs_update_ || (force_update && this->pco2_is_set_)) { + if (this->component_flags_.pco2_needs_update || (force_update && this->component_flags_.pco2_is_set)) { this->nextion_->set_component_pressed_foreground_color(this->variable_name_.c_str(), this->pco2_); - this->pco2_needs_update_ = false; + this->component_flags_.pco2_needs_update = false; } - if (this->font_id_needs_update_ || (force_update && this->font_id_is_set_)) { + if (this->component_flags_.font_id_needs_update || (force_update && this->component_flags_.font_id_is_set)) { this->nextion_->set_component_font(this->variable_name_.c_str(), this->font_id_); - this->font_id_needs_update_ = false; + this->component_flags_.font_id_needs_update = false; } } } // namespace nextion diff --git a/esphome/components/nextion/nextion_component.h b/esphome/components/nextion/nextion_component.h index 2f3c4f3c16..add9e11cf1 100644 --- a/esphome/components/nextion/nextion_component.h +++ b/esphome/components/nextion/nextion_component.h @@ -21,29 +21,64 @@ class NextionComponent : public NextionComponentBase { void set_visible(bool visible); protected: + /** + * @brief Constructor initializes component state with visible=true (default state) + */ + NextionComponent() { + component_flags_ = {}; // Zero-initialize all state + component_flags_.visible = 1; // Set default visibility to true + } + NextionBase *nextion_; - bool bco_needs_update_ = false; - bool bco_is_set_ = false; - Color bco_; - bool bco2_needs_update_ = false; - bool bco2_is_set_ = false; - Color bco2_; - bool pco_needs_update_ = false; - bool pco_is_set_ = false; - Color pco_; - bool pco2_needs_update_ = false; - bool pco2_is_set_ = false; - Color pco2_; + // Color and styling properties + Color bco_; // Background color + Color bco2_; // Pressed background color + Color pco_; // Foreground color + Color pco2_; // Pressed foreground color uint8_t font_id_ = 0; - bool font_id_needs_update_ = false; - bool font_id_is_set_ = false; - bool visible_ = true; - bool visible_needs_update_ = false; - bool visible_is_set_ = false; + /** + * @brief Component state management using compact bitfield structure + * + * Stores all component state flags and properties in a single 16-bit bitfield + * for efficient memory usage and improved cache locality. + * + * Each component property maintains two state flags: + * - needs_update: Indicates the property requires synchronization with the display + * - is_set: Tracks whether the property has been explicitly configured + * + * The visible field stores both the update flags and the actual visibility state. + */ + struct ComponentState { + // Background color flags + uint16_t bco_needs_update : 1; + uint16_t bco_is_set : 1; - // void send_state_to_nextion() = 0; + // Pressed background color flags + uint16_t bco2_needs_update : 1; + uint16_t bco2_is_set : 1; + + // Foreground color flags + uint16_t pco_needs_update : 1; + uint16_t pco_is_set : 1; + + // Pressed foreground color flags + uint16_t pco2_needs_update : 1; + uint16_t pco2_is_set : 1; + + // Font ID flags + uint16_t font_id_needs_update : 1; + uint16_t font_id_is_set : 1; + + // Visibility flags + uint16_t visible_needs_update : 1; + uint16_t visible_is_set : 1; + uint16_t visible : 1; // Actual visibility state + + // Reserved bits for future expansion + uint16_t reserved : 3; + } component_flags_; }; } // namespace nextion } // namespace esphome diff --git a/esphome/components/nextion/nextion_component_base.h b/esphome/components/nextion/nextion_component_base.h index 42e1b00998..fe0692b875 100644 --- a/esphome/components/nextion/nextion_component_base.h +++ b/esphome/components/nextion/nextion_component_base.h @@ -25,6 +25,9 @@ class NextionQueue { virtual ~NextionQueue() = default; NextionComponentBase *component; uint32_t queue_time = 0; + + // Store command for retry if spacing blocked it + std::string pending_command; // Empty if command was sent successfully }; class NextionComponentBase { diff --git a/esphome/components/nextion/nextion_upload.cpp b/esphome/components/nextion/nextion_upload.cpp new file mode 100644 index 0000000000..c47b393f99 --- /dev/null +++ b/esphome/components/nextion/nextion_upload.cpp @@ -0,0 +1,36 @@ +#include "nextion.h" + +#ifdef USE_NEXTION_TFT_UPLOAD + +#include "esphome/core/application.h" + +namespace esphome { +namespace nextion { +static const char *const TAG = "nextion.upload"; + +bool Nextion::upload_end_(bool successful) { + if (successful) { + ESP_LOGD(TAG, "Upload successful"); + delay(1500); // NOLINT + App.safe_reboot(); + } else { + ESP_LOGE(TAG, "Upload failed"); + + this->connection_state_.is_updating_ = false; + this->connection_state_.ignore_is_setup_ = false; + + uint32_t baud_rate = this->parent_->get_baud_rate(); + if (baud_rate != this->original_baud_rate_) { + ESP_LOGD(TAG, "Baud: %" PRIu32 "->%" PRIu32, baud_rate, this->original_baud_rate_); + this->parent_->set_baud_rate(this->original_baud_rate_); + this->parent_->load_settings(); + } + } + + return successful; +} + +} // namespace nextion +} // namespace esphome + +#endif // USE_NEXTION_TFT_UPLOAD diff --git a/esphome/components/nextion/nextion_upload_arduino.cpp b/esphome/components/nextion/nextion_upload_arduino.cpp index 6652e70172..b0e5d121dd 100644 --- a/esphome/components/nextion/nextion_upload_arduino.cpp +++ b/esphome/components/nextion/nextion_upload_arduino.cpp @@ -3,12 +3,12 @@ #ifdef USE_NEXTION_TFT_UPLOAD #ifdef USE_ARDUINO +#include +#include "esphome/components/network/util.h" #include "esphome/core/application.h" #include "esphome/core/defines.h" -#include "esphome/core/util.h" #include "esphome/core/log.h" -#include "esphome/components/network/util.h" -#include +#include "esphome/core/util.h" #ifdef USE_ESP32 #include @@ -52,7 +52,7 @@ int Nextion::upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start) { } // Allocate the buffer dynamically - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator allocator; uint8_t *buffer = allocator.allocate(4096); if (!buffer) { ESP_LOGE(TAG, "Buffer alloc failed"); @@ -67,8 +67,8 @@ int Nextion::upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start) { ESP_LOGV(TAG, "Fetch %" PRIu16 " bytes", buffer_size); uint16_t read_len = 0; int partial_read_len = 0; - const uint32_t start_time = millis(); - while (read_len < buffer_size && millis() - start_time < 5000) { + const uint32_t start_time = App.get_loop_component_start_time(); + while (read_len < buffer_size && App.get_loop_component_start_time() - start_time < 5000) { if (http_client.getStreamPtr()->available() > 0) { partial_read_len = http_client.getStreamPtr()->readBytes(reinterpret_cast(buffer) + read_len, buffer_size - read_len); @@ -152,7 +152,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { ESP_LOGD(TAG, "Exit reparse: %s", YESNO(exit_reparse)); ESP_LOGD(TAG, "URL: %s", this->tft_url_.c_str()); - if (this->is_updating_) { + if (this->connection_state_.is_updating_) { ESP_LOGW(TAG, "Upload in progress"); return false; } @@ -162,7 +162,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { return false; } - this->is_updating_ = true; + this->connection_state_.is_updating_ = true; if (exit_reparse) { ESP_LOGD(TAG, "Exit reparse mode"); @@ -203,7 +203,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { begin_status = http_client.begin(*this->get_wifi_client_(), this->tft_url_.c_str()); #endif // USE_ESP8266 if (!begin_status) { - this->is_updating_ = false; + this->connection_state_.is_updating_ = false; ESP_LOGD(TAG, "Connection failed"); return false; } else { @@ -254,7 +254,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { // The Nextion will ignore the upload command if it is sleeping ESP_LOGV(TAG, "Wake-up"); - this->ignore_is_setup_ = true; + this->connection_state_.ignore_is_setup_ = true; this->send_command_("sleep=0"); this->send_command_("dim=100"); delay(250); // NOLINT @@ -335,31 +335,6 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { return upload_end_(true); } -bool Nextion::upload_end_(bool successful) { - ESP_LOGD(TAG, "TFT upload done: %s", YESNO(successful)); - - if (successful) { - ESP_LOGD(TAG, "Restart"); - delay(1500); // NOLINT - App.safe_reboot(); - delay(1500); // NOLINT - } else { - ESP_LOGE(TAG, "TFT upload failed"); - - this->is_updating_ = false; - this->ignore_is_setup_ = false; - - uint32_t baud_rate = this->parent_->get_baud_rate(); - if (baud_rate != this->original_baud_rate_) { - ESP_LOGD(TAG, "Baud back: %" PRIu32 "->%" PRIu32, baud_rate, this->original_baud_rate_); - this->parent_->set_baud_rate(this->original_baud_rate_); - this->parent_->load_settings(); - } - } - - return successful; -} - #ifdef USE_ESP8266 WiFiClient *Nextion::get_wifi_client_() { if (this->tft_url_.compare(0, 6, "https:") == 0) { diff --git a/esphome/components/nextion/nextion_upload_idf.cpp b/esphome/components/nextion/nextion_upload_idf.cpp index fc98056bc3..78a47f9e2c 100644 --- a/esphome/components/nextion/nextion_upload_idf.cpp +++ b/esphome/components/nextion/nextion_upload_idf.cpp @@ -3,14 +3,14 @@ #ifdef USE_NEXTION_TFT_UPLOAD #ifdef USE_ESP_IDF -#include "esphome/core/application.h" -#include "esphome/core/defines.h" -#include "esphome/core/util.h" -#include "esphome/core/log.h" -#include "esphome/components/network/util.h" -#include #include #include +#include +#include "esphome/components/network/util.h" +#include "esphome/core/application.h" +#include "esphome/core/defines.h" +#include "esphome/core/log.h" +#include "esphome/core/util.h" namespace esphome { namespace nextion { @@ -51,7 +51,7 @@ int Nextion::upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &r } // Allocate the buffer dynamically - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator allocator; uint8_t *buffer = allocator.allocate(4096); if (!buffer) { ESP_LOGE(TAG, "Buffer alloc failed"); @@ -155,7 +155,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { ESP_LOGD(TAG, "Exit reparse: %s", YESNO(exit_reparse)); ESP_LOGD(TAG, "URL: %s", this->tft_url_.c_str()); - if (this->is_updating_) { + if (this->connection_state_.is_updating_) { ESP_LOGW(TAG, "Upload in progress"); return false; } @@ -165,7 +165,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { return false; } - this->is_updating_ = true; + this->connection_state_.is_updating_ = true; if (exit_reparse) { ESP_LOGD(TAG, "Exit reparse mode"); @@ -246,7 +246,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { // The Nextion will ignore the upload command if it is sleeping ESP_LOGV(TAG, "Wake-up"); - this->ignore_is_setup_ = true; + this->connection_state_.ignore_is_setup_ = true; this->send_command_("sleep=0"); this->send_command_("dim=100"); vTaskDelay(pdMS_TO_TICKS(250)); // NOLINT @@ -335,30 +335,6 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { return this->upload_end_(true); } -bool Nextion::upload_end_(bool successful) { - ESP_LOGD(TAG, "TFT upload done: %s", YESNO(successful)); - - if (successful) { - ESP_LOGD(TAG, "Restart"); - delay(1500); // NOLINT - App.safe_reboot(); - } else { - ESP_LOGE(TAG, "TFT upload failed"); - - this->is_updating_ = false; - this->ignore_is_setup_ = false; - - uint32_t baud_rate = this->parent_->get_baud_rate(); - if (baud_rate != this->original_baud_rate_) { - ESP_LOGD(TAG, "Baud back: %" PRIu32 "->%" PRIu32, baud_rate, this->original_baud_rate_); - this->parent_->set_baud_rate(this->original_baud_rate_); - this->parent_->load_settings(); - } - } - - return successful; -} - } // namespace nextion } // namespace esphome diff --git a/esphome/components/nextion/sensor/nextion_sensor.cpp b/esphome/components/nextion/sensor/nextion_sensor.cpp index 0ed9da95d4..03b7261239 100644 --- a/esphome/components/nextion/sensor/nextion_sensor.cpp +++ b/esphome/components/nextion/sensor/nextion_sensor.cpp @@ -53,7 +53,7 @@ void NextionSensor::set_state(float state, bool publish, bool send_to_nextion) { if (this->wave_chan_id_ == UINT8_MAX) { if (send_to_nextion) { - if (this->nextion_->is_sleeping() || !this->visible_) { + if (this->nextion_->is_sleeping() || !this->component_flags_.visible) { this->needs_to_send_update_ = true; } else { this->needs_to_send_update_ = false; diff --git a/esphome/components/nextion/switch/nextion_switch.cpp b/esphome/components/nextion/switch/nextion_switch.cpp index fe71182496..21636f2bfa 100644 --- a/esphome/components/nextion/switch/nextion_switch.cpp +++ b/esphome/components/nextion/switch/nextion_switch.cpp @@ -28,7 +28,7 @@ void NextionSwitch::set_state(bool state, bool publish, bool send_to_nextion) { return; if (send_to_nextion) { - if (this->nextion_->is_sleeping() || !this->visible_) { + if (this->nextion_->is_sleeping() || !this->component_flags_.visible) { this->needs_to_send_update_ = true; } else { this->needs_to_send_update_ = false; diff --git a/esphome/components/nextion/text_sensor/nextion_textsensor.cpp b/esphome/components/nextion/text_sensor/nextion_textsensor.cpp index e08cbb02ca..9b6deeda87 100644 --- a/esphome/components/nextion/text_sensor/nextion_textsensor.cpp +++ b/esphome/components/nextion/text_sensor/nextion_textsensor.cpp @@ -26,7 +26,7 @@ void NextionTextSensor::set_state(const std::string &state, bool publish, bool s return; if (send_to_nextion) { - if (this->nextion_->is_sleeping() || !this->visible_) { + if (this->nextion_->is_sleeping() || !this->component_flags_.visible) { this->needs_to_send_update_ = true; } else { this->nextion_->add_no_result_to_queue_with_set(this, state); diff --git a/esphome/components/nfc/nfc.cpp b/esphome/components/nfc/nfc.cpp index cf5a7f5ef1..d3a2481693 100644 --- a/esphome/components/nfc/nfc.cpp +++ b/esphome/components/nfc/nfc.cpp @@ -1,5 +1,6 @@ #include "nfc.h" #include +#include "esphome/core/helpers.h" #include "esphome/core/log.h" namespace esphome { @@ -7,29 +8,9 @@ namespace nfc { static const char *const TAG = "nfc"; -std::string format_uid(std::vector &uid) { - 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_uid(const std::vector &uid) { return format_hex_pretty(uid, '-', false); } -std::string format_bytes(std::vector &bytes) { - 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); -} +std::string format_bytes(const std::vector &bytes) { return format_hex_pretty(bytes, ' ', false); } uint8_t guess_tag_type(uint8_t uid_length) { if (uid_length == 4) { diff --git a/esphome/components/nfc/nfc.h b/esphome/components/nfc/nfc.h index 2e5c5cd9c5..9879cfdb03 100644 --- a/esphome/components/nfc/nfc.h +++ b/esphome/components/nfc/nfc.h @@ -2,8 +2,8 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include "ndef_record.h" #include "ndef_message.h" +#include "ndef_record.h" #include "nfc_tag.h" #include @@ -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 MAD_KEY[6] = {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5}; -std::string format_uid(std::vector &uid); -std::string format_bytes(std::vector &bytes); +std::string format_uid(const std::vector &uid); +std::string format_bytes(const std::vector &bytes); uint8_t guess_tag_type(uint8_t uid_length); uint8_t get_mifare_classic_ndef_start_index(std::vector &data); diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index 2567d9ffe1..4beed57188 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -76,8 +76,8 @@ from esphome.const import ( DEVICE_CLASS_WIND_SPEED, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@esphome/core"] DEVICE_CLASSES = [ @@ -207,6 +207,9 @@ _NUMBER_SCHEMA = ( ) +_NUMBER_SCHEMA.add_extra(entity_duplicate_validator("number")) + + def number_schema( class_: MockObjClass, *, @@ -237,7 +240,7 @@ NUMBER_SCHEMA.add_extra(cv.deprecated_schema_constant("number")) async def setup_number_core_( var, config, *, min_value: float, max_value: float, step: float ): - await setup_entity(var, config) + await setup_entity(var, config, "number") cg.add(var.traits.set_min_value(min_value)) cg.add(var.traits.set_max_value(max_value)) diff --git a/esphome/components/online_image/__init__.py b/esphome/components/online_image/__init__.py index 9380cf1b1b..3f15db6e50 100644 --- a/esphome/components/online_image/__init__.py +++ b/esphome/components/online_image/__init__.py @@ -34,6 +34,7 @@ MULTI_CONF = True CONF_ON_DOWNLOAD_FINISHED = "on_download_finished" CONF_PLACEHOLDER = "placeholder" +CONF_UPDATE = "update" _LOGGER = logging.getLogger(__name__) @@ -167,6 +168,7 @@ SET_URL_SCHEMA = cv.Schema( { cv.GenerateID(): cv.use_id(OnlineImage), cv.Required(CONF_URL): cv.templatable(cv.url), + cv.Optional(CONF_UPDATE, default=True): cv.templatable(bool), } ) @@ -188,6 +190,9 @@ async def online_image_action_to_code(config, action_id, template_arg, args): if CONF_URL in config: template_ = await cg.templatable(config[CONF_URL], args, cg.std_string) cg.add(var.set_url(template_)) + if CONF_UPDATE in config: + template_ = await cg.templatable(config[CONF_UPDATE], args, bool) + cg.add(var.set_update(template_)) return var diff --git a/esphome/components/online_image/online_image.cpp b/esphome/components/online_image/online_image.cpp index 8030bd0095..d0c743ef93 100644 --- a/esphome/components/online_image/online_image.cpp +++ b/esphome/components/online_image/online_image.cpp @@ -178,18 +178,21 @@ void OnlineImage::update() { if (this->format_ == ImageFormat::BMP) { ESP_LOGD(TAG, "Allocating BMP decoder"); this->decoder_ = make_unique(this); + this->enable_loop(); } #endif // USE_ONLINE_IMAGE_BMP_SUPPORT #ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT if (this->format_ == ImageFormat::JPEG) { ESP_LOGD(TAG, "Allocating JPEG decoder"); this->decoder_ = esphome::make_unique(this); + this->enable_loop(); } #endif // USE_ONLINE_IMAGE_JPEG_SUPPORT #ifdef USE_ONLINE_IMAGE_PNG_SUPPORT if (this->format_ == ImageFormat::PNG) { ESP_LOGD(TAG, "Allocating PNG decoder"); this->decoder_ = make_unique(this); + this->enable_loop(); } #endif // USE_ONLINE_IMAGE_PNG_SUPPORT @@ -212,6 +215,7 @@ void OnlineImage::update() { void OnlineImage::loop() { if (!this->decoder_) { // Not decoding at the moment => nothing to do. + this->disable_loop(); return; } if (!this->downloader_ || this->decoder_->is_finished()) { @@ -220,7 +224,7 @@ void OnlineImage::loop() { this->height_ = buffer_height_; ESP_LOGD(TAG, "Image fully downloaded, read %zu bytes, width/height = %d/%d", this->downloader_->get_bytes_read(), this->width_, this->height_); - ESP_LOGD(TAG, "Total time: %lds", ::time(nullptr) - this->start_time_); + ESP_LOGD(TAG, "Total time: %" PRIu32 "s", (uint32_t) (::time(nullptr) - this->start_time_)); this->etag_ = this->downloader_->get_response_header(ETAG_HEADER_NAME); this->last_modified_ = this->downloader_->get_response_header(LAST_MODIFIED_HEADER_NAME); this->download_finished_callback_.call(false); @@ -340,7 +344,7 @@ void OnlineImage::end_connection_() { } bool OnlineImage::validate_url_(const std::string &url) { - if ((url.length() < 8) || (url.find("http") != 0) || (url.find("://") == std::string::npos)) { + if ((url.length() < 8) || !url.starts_with("http") || (url.find("://") == std::string::npos)) { ESP_LOGE(TAG, "URL is invalid and/or must be prefixed with 'http://' or 'https://'"); return false; } diff --git a/esphome/components/online_image/online_image.h b/esphome/components/online_image/online_image.h index 6ed9c7956f..6a2144538f 100644 --- a/esphome/components/online_image/online_image.h +++ b/esphome/components/online_image/online_image.h @@ -201,9 +201,12 @@ template class OnlineImageSetUrlAction : public Action { public: OnlineImageSetUrlAction(OnlineImage *parent) : parent_(parent) {} TEMPLATABLE_VALUE(std::string, url) + TEMPLATABLE_VALUE(bool, update) void play(Ts... x) override { this->parent_->set_url(this->url_.value(x...)); - this->parent_->update(); + if (this->update_.value(x...)) { + this->parent_->update(); + } } protected: diff --git a/esphome/components/opentherm/opentherm.cpp b/esphome/components/opentherm/opentherm.cpp index 49482316ee..b2751470b2 100644 --- a/esphome/components/opentherm/opentherm.cpp +++ b/esphome/components/opentherm/opentherm.cpp @@ -272,18 +272,13 @@ bool OpenTherm::init_esp32_timer_() { this->timer_idx_ = timer_idx; timer_config_t const config = { - .alarm_en = TIMER_ALARM_EN, - .counter_en = TIMER_PAUSE, - .intr_type = TIMER_INTR_LEVEL, - .counter_dir = TIMER_COUNT_UP, - .auto_reload = TIMER_AUTORELOAD_EN, -#if ESP_IDF_VERSION_MAJOR >= 5 - .clk_src = TIMER_SRC_CLK_DEFAULT, -#endif - .divider = 80, -#if defined(SOC_TIMER_GROUP_SUPPORT_XTAL) && ESP_IDF_VERSION_MAJOR < 5 - .clk_src = TIMER_SRC_CLK_APB -#endif + .alarm_en = TIMER_ALARM_EN, + .counter_en = TIMER_PAUSE, + .intr_type = TIMER_INTR_LEVEL, + .counter_dir = TIMER_COUNT_UP, + .auto_reload = TIMER_AUTORELOAD_EN, + .clk_src = TIMER_SRC_CLK_DEFAULT, + .divider = 80, }; esp_err_t result; diff --git a/esphome/components/openthread/__init__.py b/esphome/components/openthread/__init__.py index 393c47e720..25e3153d1b 100644 --- a/esphome/components/openthread/__init__.py +++ b/esphome/components/openthread/__init__.py @@ -11,6 +11,7 @@ from esphome.const import CONF_CHANNEL, CONF_ENABLE_IPV6, CONF_ID import esphome.final_validate as fv from .const import ( + CONF_DEVICE_TYPE, CONF_EXT_PAN_ID, CONF_FORCE_DATASET, CONF_MDNS_ID, @@ -22,7 +23,6 @@ from .const import ( CONF_SRP_ID, CONF_TLV, ) -from .tlv import parse_tlv CODEOWNERS = ["@mrene"] @@ -33,6 +33,11 @@ AUTO_LOAD = ["network"] CONFLICTS_WITH = ["wifi"] DEPENDENCIES = ["esp32"] +CONF_DEVICE_TYPES = [ + "FTD", + "MTD", +] + def set_sdkconfig_options(config): # and expose options for using SPI/UART RCPs @@ -43,58 +48,58 @@ def set_sdkconfig_options(config): add_idf_sdkconfig_option("CONFIG_OPENTHREAD_CLI", False) add_idf_sdkconfig_option("CONFIG_OPENTHREAD_ENABLED", True) - add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_PANID", config[CONF_PAN_ID]) - add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_CHANNEL", config[CONF_CHANNEL]) - add_idf_sdkconfig_option( - "CONFIG_OPENTHREAD_NETWORK_MASTERKEY", f"{config[CONF_NETWORK_KEY]:X}".lower() - ) - if network_name := config.get(CONF_NETWORK_NAME): - add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_NAME", network_name) + if tlv := config.get(CONF_TLV): + cg.add_define("USE_OPENTHREAD_TLVS", tlv) + else: + if pan_id := config.get(CONF_PAN_ID): + add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_PANID", pan_id) - if (ext_pan_id := config.get(CONF_EXT_PAN_ID)) is not None: - add_idf_sdkconfig_option( - "CONFIG_OPENTHREAD_NETWORK_EXTPANID", f"{ext_pan_id:X}".lower() - ) - if (mesh_local_prefix := config.get(CONF_MESH_LOCAL_PREFIX)) is not None: - add_idf_sdkconfig_option( - "CONFIG_OPENTHREAD_MESH_LOCAL_PREFIX", f"{mesh_local_prefix}".lower() - ) - if (pskc := config.get(CONF_PSKC)) is not None: - add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_PSKC", f"{pskc:X}".lower()) + if channel := config.get(CONF_CHANNEL): + add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_CHANNEL", channel) - if CONF_FORCE_DATASET in config: - if config[CONF_FORCE_DATASET]: - cg.add_define("CONFIG_OPENTHREAD_FORCE_DATASET") + if network_key := config.get(CONF_NETWORK_KEY): + add_idf_sdkconfig_option( + "CONFIG_OPENTHREAD_NETWORK_MASTERKEY", f"{network_key:X}".lower() + ) + + if network_name := config.get(CONF_NETWORK_NAME): + add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_NAME", network_name) + + if (ext_pan_id := config.get(CONF_EXT_PAN_ID)) is not None: + add_idf_sdkconfig_option( + "CONFIG_OPENTHREAD_NETWORK_EXTPANID", f"{ext_pan_id:X}".lower() + ) + if (mesh_local_prefix := config.get(CONF_MESH_LOCAL_PREFIX)) is not None: + add_idf_sdkconfig_option( + "CONFIG_OPENTHREAD_MESH_LOCAL_PREFIX", f"{mesh_local_prefix}".lower() + ) + if (pskc := config.get(CONF_PSKC)) is not None: + add_idf_sdkconfig_option( + "CONFIG_OPENTHREAD_NETWORK_PSKC", f"{pskc:X}".lower() + ) + + if force_dataset := config.get(CONF_FORCE_DATASET): + if force_dataset: + cg.add_define("USE_OPENTHREAD_FORCE_DATASET") add_idf_sdkconfig_option("CONFIG_OPENTHREAD_DNS64_CLIENT", True) add_idf_sdkconfig_option("CONFIG_OPENTHREAD_SRP_CLIENT", True) add_idf_sdkconfig_option("CONFIG_OPENTHREAD_SRP_CLIENT_MAX_SERVICES", 5) # TODO: Add suport for sleepy end devices - add_idf_sdkconfig_option("CONFIG_OPENTHREAD_FTD", True) # Full Thread Device + add_idf_sdkconfig_option(f"CONFIG_OPENTHREAD_{config.get(CONF_DEVICE_TYPE)}", True) openthread_ns = cg.esphome_ns.namespace("openthread") OpenThreadComponent = openthread_ns.class_("OpenThreadComponent", cg.Component) OpenThreadSrpComponent = openthread_ns.class_("OpenThreadSrpComponent", cg.Component) - -def _convert_tlv(config): - if tlv := config.get(CONF_TLV): - config = config.copy() - parsed_tlv = parse_tlv(tlv) - validated = _CONNECTION_SCHEMA(parsed_tlv) - config.update(validated) - del config[CONF_TLV] - return config - - _CONNECTION_SCHEMA = cv.Schema( { - cv.Inclusive(CONF_PAN_ID, "manual"): cv.hex_int, - cv.Inclusive(CONF_CHANNEL, "manual"): cv.int_, - cv.Inclusive(CONF_NETWORK_KEY, "manual"): cv.hex_int, + cv.Optional(CONF_PAN_ID): cv.hex_int, + cv.Optional(CONF_CHANNEL): cv.int_, + cv.Optional(CONF_NETWORK_KEY): cv.hex_int, cv.Optional(CONF_EXT_PAN_ID): cv.hex_int, cv.Optional(CONF_NETWORK_NAME): cv.string_strict, cv.Optional(CONF_PSKC): cv.hex_int, @@ -108,12 +113,14 @@ CONFIG_SCHEMA = cv.All( cv.GenerateID(): cv.declare_id(OpenThreadComponent), cv.GenerateID(CONF_SRP_ID): cv.declare_id(OpenThreadSrpComponent), cv.GenerateID(CONF_MDNS_ID): cv.use_id(MDNSComponent), + cv.Optional(CONF_DEVICE_TYPE, default="FTD"): cv.one_of( + *CONF_DEVICE_TYPES, upper=True + ), cv.Optional(CONF_FORCE_DATASET): cv.boolean, cv.Optional(CONF_TLV): cv.string_strict, } ).extend(_CONNECTION_SCHEMA), - cv.has_exactly_one_key(CONF_PAN_ID, CONF_TLV), - _convert_tlv, + cv.has_exactly_one_key(CONF_NETWORK_KEY, CONF_TLV), cv.only_with_esp_idf, only_on_variant(supported=[VARIANT_ESP32C6, VARIANT_ESP32H2]), ) diff --git a/esphome/components/openthread/const.py b/esphome/components/openthread/const.py index 7837e69eea..7a6ffb2df4 100644 --- a/esphome/components/openthread/const.py +++ b/esphome/components/openthread/const.py @@ -1,3 +1,4 @@ +CONF_DEVICE_TYPE = "device_type" CONF_EXT_PAN_ID = "ext_pan_id" CONF_FORCE_DATASET = "force_dataset" CONF_MDNS_ID = "mdns_id" diff --git a/esphome/components/openthread/openthread_esp.cpp b/esphome/components/openthread/openthread_esp.cpp index c5c817382f..dc303cef17 100644 --- a/esphome/components/openthread/openthread_esp.cpp +++ b/esphome/components/openthread/openthread_esp.cpp @@ -111,14 +111,36 @@ void OpenThreadComponent::ot_main() { esp_openthread_cli_create_task(); #endif ESP_LOGI(TAG, "Activating dataset..."); - otOperationalDatasetTlvs dataset; + otOperationalDatasetTlvs dataset = {}; -#ifdef CONFIG_OPENTHREAD_FORCE_DATASET - ESP_ERROR_CHECK(esp_openthread_auto_start(NULL)); -#else +#ifndef USE_OPENTHREAD_FORCE_DATASET + // Check if openthread has a valid dataset from a previous execution otError error = otDatasetGetActiveTlvs(esp_openthread_get_instance(), &dataset); - ESP_ERROR_CHECK(esp_openthread_auto_start((error == OT_ERROR_NONE) ? &dataset : NULL)); + if (error != OT_ERROR_NONE) { + // Make sure the length is 0 so we fallback to the configuration + dataset.mLength = 0; + } else { + ESP_LOGI(TAG, "Found OpenThread-managed dataset, ignoring esphome configuration"); + ESP_LOGI(TAG, "(set force_dataset: true to override)"); + } #endif + +#ifdef USE_OPENTHREAD_TLVS + if (dataset.mLength == 0) { + // If we didn't have an active dataset, and we have tlvs, parse it and pass it to esp_openthread_auto_start + size_t len = (sizeof(USE_OPENTHREAD_TLVS) - 1) / 2; + if (len > sizeof(dataset.mTlvs)) { + ESP_LOGW(TAG, "TLV buffer too small, truncating"); + len = sizeof(dataset.mTlvs); + } + parse_hex(USE_OPENTHREAD_TLVS, sizeof(USE_OPENTHREAD_TLVS) - 1, dataset.mTlvs, len); + dataset.mLength = len; + } +#endif + + // Pass the existing dataset, or NULL which will use the preprocessor definitions + ESP_ERROR_CHECK(esp_openthread_auto_start(dataset.mLength > 0 ? &dataset : nullptr)); + esp_openthread_launch_mainloop(); // Clean up diff --git a/esphome/components/openthread/tlv.py b/esphome/components/openthread/tlv.py deleted file mode 100644 index 4a7d21c47d..0000000000 --- a/esphome/components/openthread/tlv.py +++ /dev/null @@ -1,65 +0,0 @@ -# Sourced from https://gist.github.com/agners/0338576e0003318b63ec1ea75adc90f9 -import binascii -import ipaddress - -from esphome.const import CONF_CHANNEL - -from . import ( - CONF_EXT_PAN_ID, - CONF_MESH_LOCAL_PREFIX, - CONF_NETWORK_KEY, - CONF_NETWORK_NAME, - CONF_PAN_ID, - CONF_PSKC, -) - -TLV_TYPES = { - 0: CONF_CHANNEL, - 1: CONF_PAN_ID, - 2: CONF_EXT_PAN_ID, - 3: CONF_NETWORK_NAME, - 4: CONF_PSKC, - 5: CONF_NETWORK_KEY, - 7: CONF_MESH_LOCAL_PREFIX, -} - - -def parse_tlv(tlv) -> dict: - data = binascii.a2b_hex(tlv) - output = {} - pos = 0 - while pos < len(data): - tag = data[pos] - pos += 1 - _len = data[pos] - pos += 1 - val = data[pos : pos + _len] - pos += _len - if tag in TLV_TYPES: - if tag == 3: - output[TLV_TYPES[tag]] = val.decode("utf-8") - elif tag == 7: - mesh_local_prefix = binascii.hexlify(val).decode("utf-8") - mesh_local_prefix_str = f"{mesh_local_prefix}0000000000000000" - ipv6_bytes = bytes.fromhex(mesh_local_prefix_str) - ipv6_address = ipaddress.IPv6Address(ipv6_bytes) - output[TLV_TYPES[tag]] = f"{ipv6_address}/64" - else: - output[TLV_TYPES[tag]] = int.from_bytes(val) - return output - - -def main(): - import sys - - args = sys.argv[1:] - parsed = parse_tlv(args[0]) - # print the parsed TLV data - for key, value in parsed.items(): - if isinstance(value, bytes): - value = value.hex() - print(f"{key}: {value}") - - -if __name__ == "__main__": - main() diff --git a/esphome/components/opt3001/__init__.py b/esphome/components/opt3001/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/opt3001/opt3001.cpp b/esphome/components/opt3001/opt3001.cpp new file mode 100644 index 0000000000..2d65f1090d --- /dev/null +++ b/esphome/components/opt3001/opt3001.cpp @@ -0,0 +1,122 @@ +#include "opt3001.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace opt3001 { + +static const char *const TAG = "opt3001.sensor"; + +static const uint8_t OPT3001_REG_RESULT = 0x00; +static const uint8_t OPT3001_REG_CONFIGURATION = 0x01; +// See datasheet for full description of each bit. +static const uint16_t OPT3001_CONFIGURATION_RANGE_FULL = 0b1100000000000000; +static const uint16_t OPT3001_CONFIGURATION_CONVERSION_TIME_800 = 0b100000000000; +static const uint16_t OPT3001_CONFIGURATION_CONVERSION_MODE_MASK = 0b11000000000; +static const uint16_t OPT3001_CONFIGURATION_CONVERSION_MODE_SINGLE_SHOT = 0b01000000000; +static const uint16_t OPT3001_CONFIGURATION_CONVERSION_MODE_SHUTDOWN = 0b00000000000; +// tl;dr: Configure an automatic-ranged, 800ms single shot reading, +// with INT processing disabled +static const uint16_t OPT3001_CONFIGURATION_FULL_RANGE_ONE_SHOT = OPT3001_CONFIGURATION_RANGE_FULL | + OPT3001_CONFIGURATION_CONVERSION_TIME_800 | + OPT3001_CONFIGURATION_CONVERSION_MODE_SINGLE_SHOT; +static const uint16_t OPT3001_CONVERSION_TIME_800 = 825; // give it 25 extra ms; it seems to not be ready quite often + +/* +opt3001 properties: + +- e (exponent) = high 4 bits of result register +- m (mantissa) = low 12 bits of result register +- formula: (0.01 * 2^e) * m lx + +*/ + +void OPT3001Sensor::read_result_(const std::function &f) { + // ensure the single shot flag is clear, indicating it's done + uint16_t raw_value; + if (this->read(reinterpret_cast(&raw_value), 2) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Reading configuration register failed"); + f(NAN); + return; + } + raw_value = i2c::i2ctohs(raw_value); + + if ((raw_value & OPT3001_CONFIGURATION_CONVERSION_MODE_MASK) != OPT3001_CONFIGURATION_CONVERSION_MODE_SHUTDOWN) { + // not ready; wait 10ms and try again + ESP_LOGW(TAG, "Data not ready; waiting 10ms"); + this->set_timeout("opt3001_wait", 10, [this, f]() { read_result_(f); }); + return; + } + + if (this->read_register(OPT3001_REG_RESULT, reinterpret_cast(&raw_value), 2) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Reading result register failed"); + f(NAN); + return; + } + raw_value = i2c::i2ctohs(raw_value); + + uint8_t exponent = raw_value >> 12; + uint16_t mantissa = raw_value & 0b111111111111; + + double lx = 0.01 * pow(2.0, double(exponent)) * double(mantissa); + f(float(lx)); +} + +void OPT3001Sensor::read_lx_(const std::function &f) { + // turn on (after one-shot sensor automatically powers down) + uint16_t start_measurement = i2c::htoi2cs(OPT3001_CONFIGURATION_FULL_RANGE_ONE_SHOT); + if (this->write_register(OPT3001_REG_CONFIGURATION, reinterpret_cast(&start_measurement), 2) != + i2c::ERROR_OK) { + ESP_LOGW(TAG, "Triggering one shot measurement failed"); + f(NAN); + return; + } + + this->set_timeout("read", OPT3001_CONVERSION_TIME_800, [this, f]() { + if (this->write(&OPT3001_REG_CONFIGURATION, 1, true) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Starting configuration register read failed"); + f(NAN); + return; + } + + this->read_result_(f); + }); +} + +void OPT3001Sensor::dump_config() { + LOG_SENSOR("", "OPT3001", this); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); + } + + LOG_UPDATE_INTERVAL(this); +} + +void OPT3001Sensor::update() { + // Set a flag and skip just in case the sensor isn't responding, + // and we just keep waiting for it in read_result_. + // This way we don't end up with potentially boundless "threads" + // using up memory and eventually crashing the device + if (this->updating_) { + return; + } + this->updating_ = true; + + this->read_lx_([this](float val) { + this->updating_ = false; + + if (std::isnan(val)) { + this->status_set_warning(); + this->publish_state(NAN); + return; + } + ESP_LOGD(TAG, "'%s': Illuminance=%.1flx", this->get_name().c_str(), val); + this->status_clear_warning(); + this->publish_state(val); + }); +} + +float OPT3001Sensor::get_setup_priority() const { return setup_priority::DATA; } + +} // namespace opt3001 +} // namespace esphome diff --git a/esphome/components/opt3001/opt3001.h b/esphome/components/opt3001/opt3001.h new file mode 100644 index 0000000000..ae3fde5c54 --- /dev/null +++ b/esphome/components/opt3001/opt3001.h @@ -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 opt3001 { + +/// This class implements support for the i2c-based OPT3001 ambient light sensor. +class OPT3001Sensor : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { + public: + void dump_config() override; + void update() override; + float get_setup_priority() const override; + + protected: + // checks if one-shot is complete before reading the result and returning it + void read_result_(const std::function &f); + // begins a one-shot measurement + void read_lx_(const std::function &f); + + bool updating_{false}; +}; + +} // namespace opt3001 +} // namespace esphome diff --git a/esphome/components/opt3001/sensor.py b/esphome/components/opt3001/sensor.py new file mode 100644 index 0000000000..a5bbf0e8dd --- /dev/null +++ b/esphome/components/opt3001/sensor.py @@ -0,0 +1,35 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + DEVICE_CLASS_ILLUMINANCE, + STATE_CLASS_MEASUREMENT, + UNIT_LUX, +) + +DEPENDENCIES = ["i2c"] +CODEOWNERS = ["@ccutrer"] + +opt3001_ns = cg.esphome_ns.namespace("opt3001") + +OPT3001Sensor = opt3001_ns.class_( + "OPT3001Sensor", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + OPT3001Sensor, + unit_of_measurement=UNIT_LUX, + accuracy_decimals=1, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x44)) +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/ota/__init__.py b/esphome/components/ota/__init__.py index 627c55e910..4d5b8a61e2 100644 --- a/esphome/components/ota/__init__.py +++ b/esphome/components/ota/__init__.py @@ -1,5 +1,6 @@ from esphome import automation import esphome.codegen as cg +from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( CONF_ESPHOME, @@ -7,6 +8,7 @@ from esphome.const import ( CONF_OTA, CONF_PLATFORM, CONF_TRIGGER_ID, + PlatformFramework, ) from esphome.core import CORE, coroutine_with_priority @@ -120,3 +122,18 @@ async def ota_to_code(var, config): use_state_callback = True if use_state_callback: cg.add_define("USE_OTA_STATE_CALLBACK") + + +FILTER_SOURCE_FILES = filter_source_files_from_platform( + { + "ota_backend_arduino_esp32.cpp": {PlatformFramework.ESP32_ARDUINO}, + "ota_backend_esp_idf.cpp": {PlatformFramework.ESP32_IDF}, + "ota_backend_arduino_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, + "ota_backend_arduino_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO}, + "ota_backend_arduino_libretiny.cpp": { + PlatformFramework.BK72XX_ARDUINO, + PlatformFramework.RTL87XX_ARDUINO, + PlatformFramework.LN882X_ARDUINO, + }, + } +) diff --git a/esphome/components/ota/ota_backend.h b/esphome/components/ota/ota_backend.h index bc8ab46643..372f24df5e 100644 --- a/esphome/components/ota/ota_backend.h +++ b/esphome/components/ota/ota_backend.h @@ -67,7 +67,28 @@ class OTAComponent : public Component { } protected: - CallbackManager state_callback_{}; + /** Extended callback manager with deferred call support. + * + * This adds a call_deferred() method for thread-safe execution from other tasks. + */ + class StateCallbackManager : public CallbackManager { + public: + StateCallbackManager(OTAComponent *component) : component_(component) {} + + /** Call callbacks with deferral to main loop (for thread safety). + * + * This should be used by OTA implementations that run in separate tasks + * (like web_server OTA) to ensure callbacks execute in the main loop. + */ + void call_deferred(ota::OTAState state, float progress, uint8_t error) { + component_->defer([this, state, progress, error]() { this->call(state, progress, error); }); + } + + private: + OTAComponent *component_; + }; + + StateCallbackManager state_callback_{this}; #endif }; @@ -89,6 +110,11 @@ class OTAGlobalCallback { OTAGlobalCallback *get_global_ota_callback(); void register_ota_platform(OTAComponent *ota_caller); + +// OTA implementations should use: +// - state_callback_.call() when already in main loop (e.g., esphome OTA) +// - state_callback_.call_deferred() when in separate task (e.g., web_server OTA) +// This ensures proper callback execution in all contexts. #endif std::unique_ptr make_ota_backend(); diff --git a/esphome/components/ota/ota_backend_arduino_esp32.cpp b/esphome/components/ota/ota_backend_arduino_esp32.cpp index 15dfc98a6c..5c6230f2ce 100644 --- a/esphome/components/ota/ota_backend_arduino_esp32.cpp +++ b/esphome/components/ota/ota_backend_arduino_esp32.cpp @@ -15,6 +15,11 @@ static const char *const TAG = "ota.arduino_esp32"; std::unique_ptr make_ota_backend() { return make_unique(); } OTAResponseTypes ArduinoESP32OTABackend::begin(size_t image_size) { + // Handle UPDATE_SIZE_UNKNOWN (0) which is used by web server OTA + // where the exact firmware size is unknown due to multipart encoding + if (image_size == 0) { + image_size = UPDATE_SIZE_UNKNOWN; + } bool ret = Update.begin(image_size, U_FLASH); if (ret) { return OTA_RESPONSE_OK; @@ -29,7 +34,10 @@ OTAResponseTypes ArduinoESP32OTABackend::begin(size_t image_size) { return OTA_RESPONSE_ERROR_UNKNOWN; } -void ArduinoESP32OTABackend::set_update_md5(const char *md5) { Update.setMD5(md5); } +void ArduinoESP32OTABackend::set_update_md5(const char *md5) { + Update.setMD5(md5); + this->md5_set_ = true; +} OTAResponseTypes ArduinoESP32OTABackend::write(uint8_t *data, size_t len) { size_t written = Update.write(data, len); @@ -44,7 +52,9 @@ OTAResponseTypes ArduinoESP32OTABackend::write(uint8_t *data, size_t len) { } OTAResponseTypes ArduinoESP32OTABackend::end() { - if (Update.end()) { + // Use strict validation (false) when MD5 is set, lenient validation (true) when no MD5 + // This matches the behavior of the old web_server OTA implementation + if (Update.end(!this->md5_set_)) { return OTA_RESPONSE_OK; } diff --git a/esphome/components/ota/ota_backend_arduino_esp32.h b/esphome/components/ota/ota_backend_arduino_esp32.h index ac7fe9f14f..6615cf3dc0 100644 --- a/esphome/components/ota/ota_backend_arduino_esp32.h +++ b/esphome/components/ota/ota_backend_arduino_esp32.h @@ -16,6 +16,9 @@ class ArduinoESP32OTABackend : public OTABackend { OTAResponseTypes end() override; void abort() override; bool supports_compression() override { return false; } + + private: + bool md5_set_{false}; }; } // namespace ota diff --git a/esphome/components/ota/ota_backend_arduino_esp8266.cpp b/esphome/components/ota/ota_backend_arduino_esp8266.cpp index 42edbf5d2b..375c4e7200 100644 --- a/esphome/components/ota/ota_backend_arduino_esp8266.cpp +++ b/esphome/components/ota/ota_backend_arduino_esp8266.cpp @@ -17,6 +17,11 @@ static const char *const TAG = "ota.arduino_esp8266"; std::unique_ptr make_ota_backend() { return make_unique(); } OTAResponseTypes ArduinoESP8266OTABackend::begin(size_t image_size) { + // Handle UPDATE_SIZE_UNKNOWN (0) by calculating available space + if (image_size == 0) { + // NOLINTNEXTLINE(readability-static-accessed-through-instance) + image_size = (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000; + } bool ret = Update.begin(image_size, U_FLASH); if (ret) { esp8266::preferences_prevent_write(true); @@ -38,7 +43,10 @@ OTAResponseTypes ArduinoESP8266OTABackend::begin(size_t image_size) { return OTA_RESPONSE_ERROR_UNKNOWN; } -void ArduinoESP8266OTABackend::set_update_md5(const char *md5) { Update.setMD5(md5); } +void ArduinoESP8266OTABackend::set_update_md5(const char *md5) { + Update.setMD5(md5); + this->md5_set_ = true; +} OTAResponseTypes ArduinoESP8266OTABackend::write(uint8_t *data, size_t len) { size_t written = Update.write(data, len); @@ -53,13 +61,19 @@ OTAResponseTypes ArduinoESP8266OTABackend::write(uint8_t *data, size_t len) { } OTAResponseTypes ArduinoESP8266OTABackend::end() { - if (Update.end()) { + // Use strict validation (false) when MD5 is set, lenient validation (true) when no MD5 + // This matches the behavior of the old web_server OTA implementation + bool success = Update.end(!this->md5_set_); + + // On ESP8266, Update.end() might return false even with error code 0 + // Check the actual error code to determine success + uint8_t error = Update.getError(); + + if (success || error == UPDATE_ERROR_OK) { return OTA_RESPONSE_OK; } - uint8_t error = Update.getError(); ESP_LOGE(TAG, "End error: %d", error); - return OTA_RESPONSE_ERROR_UPDATE_END; } diff --git a/esphome/components/ota/ota_backend_arduino_esp8266.h b/esphome/components/ota/ota_backend_arduino_esp8266.h index 7f44d7c965..e1b9015cc7 100644 --- a/esphome/components/ota/ota_backend_arduino_esp8266.h +++ b/esphome/components/ota/ota_backend_arduino_esp8266.h @@ -21,6 +21,9 @@ class ArduinoESP8266OTABackend : public OTABackend { #else bool supports_compression() override { return false; } #endif + + private: + bool md5_set_{false}; }; } // namespace ota diff --git a/esphome/components/ota/ota_backend_arduino_libretiny.cpp b/esphome/components/ota/ota_backend_arduino_libretiny.cpp index 6b2cf80684..b4ecad1227 100644 --- a/esphome/components/ota/ota_backend_arduino_libretiny.cpp +++ b/esphome/components/ota/ota_backend_arduino_libretiny.cpp @@ -15,6 +15,11 @@ static const char *const TAG = "ota.arduino_libretiny"; std::unique_ptr make_ota_backend() { return make_unique(); } OTAResponseTypes ArduinoLibreTinyOTABackend::begin(size_t image_size) { + // Handle UPDATE_SIZE_UNKNOWN (0) which is used by web server OTA + // where the exact firmware size is unknown due to multipart encoding + if (image_size == 0) { + image_size = UPDATE_SIZE_UNKNOWN; + } bool ret = Update.begin(image_size, U_FLASH); if (ret) { return OTA_RESPONSE_OK; @@ -29,7 +34,10 @@ OTAResponseTypes ArduinoLibreTinyOTABackend::begin(size_t image_size) { return OTA_RESPONSE_ERROR_UNKNOWN; } -void ArduinoLibreTinyOTABackend::set_update_md5(const char *md5) { Update.setMD5(md5); } +void ArduinoLibreTinyOTABackend::set_update_md5(const char *md5) { + Update.setMD5(md5); + this->md5_set_ = true; +} OTAResponseTypes ArduinoLibreTinyOTABackend::write(uint8_t *data, size_t len) { size_t written = Update.write(data, len); @@ -44,7 +52,9 @@ OTAResponseTypes ArduinoLibreTinyOTABackend::write(uint8_t *data, size_t len) { } OTAResponseTypes ArduinoLibreTinyOTABackend::end() { - if (Update.end()) { + // Use strict validation (false) when MD5 is set, lenient validation (true) when no MD5 + // This matches the behavior of the old web_server OTA implementation + if (Update.end(!this->md5_set_)) { return OTA_RESPONSE_OK; } diff --git a/esphome/components/ota/ota_backend_arduino_libretiny.h b/esphome/components/ota/ota_backend_arduino_libretiny.h index 11deb6e2f2..6d9b7a96d5 100644 --- a/esphome/components/ota/ota_backend_arduino_libretiny.h +++ b/esphome/components/ota/ota_backend_arduino_libretiny.h @@ -15,6 +15,9 @@ class ArduinoLibreTinyOTABackend : public OTABackend { OTAResponseTypes end() override; void abort() override; bool supports_compression() override { return false; } + + private: + bool md5_set_{false}; }; } // namespace ota diff --git a/esphome/components/ota/ota_backend_arduino_rp2040.cpp b/esphome/components/ota/ota_backend_arduino_rp2040.cpp index ffeab2e93f..ee1ba48d50 100644 --- a/esphome/components/ota/ota_backend_arduino_rp2040.cpp +++ b/esphome/components/ota/ota_backend_arduino_rp2040.cpp @@ -17,6 +17,8 @@ static const char *const TAG = "ota.arduino_rp2040"; std::unique_ptr make_ota_backend() { return make_unique(); } OTAResponseTypes ArduinoRP2040OTABackend::begin(size_t image_size) { + // OTA size of 0 is not currently handled, but + // web_server is not supported for RP2040, so this is not an issue. bool ret = Update.begin(image_size, U_FLASH); if (ret) { rp2040::preferences_prevent_write(true); @@ -38,7 +40,10 @@ OTAResponseTypes ArduinoRP2040OTABackend::begin(size_t image_size) { return OTA_RESPONSE_ERROR_UNKNOWN; } -void ArduinoRP2040OTABackend::set_update_md5(const char *md5) { Update.setMD5(md5); } +void ArduinoRP2040OTABackend::set_update_md5(const char *md5) { + Update.setMD5(md5); + this->md5_set_ = true; +} OTAResponseTypes ArduinoRP2040OTABackend::write(uint8_t *data, size_t len) { size_t written = Update.write(data, len); @@ -53,7 +58,9 @@ OTAResponseTypes ArduinoRP2040OTABackend::write(uint8_t *data, size_t len) { } OTAResponseTypes ArduinoRP2040OTABackend::end() { - if (Update.end()) { + // Use strict validation (false) when MD5 is set, lenient validation (true) when no MD5 + // This matches the behavior of the old web_server OTA implementation + if (Update.end(!this->md5_set_)) { return OTA_RESPONSE_OK; } diff --git a/esphome/components/ota/ota_backend_arduino_rp2040.h b/esphome/components/ota/ota_backend_arduino_rp2040.h index b189964ab3..b9e10d506c 100644 --- a/esphome/components/ota/ota_backend_arduino_rp2040.h +++ b/esphome/components/ota/ota_backend_arduino_rp2040.h @@ -17,6 +17,9 @@ class ArduinoRP2040OTABackend : public OTABackend { OTAResponseTypes end() override; void abort() override; bool supports_compression() override { return false; } + + private: + bool md5_set_{false}; }; } // namespace ota diff --git a/esphome/components/ota/ota_backend_esp_idf.cpp b/esphome/components/ota/ota_backend_esp_idf.cpp index 6f45fb75e4..97aae09bd9 100644 --- a/esphome/components/ota/ota_backend_esp_idf.cpp +++ b/esphome/components/ota/ota_backend_esp_idf.cpp @@ -6,10 +6,7 @@ #include #include - -#if ESP_IDF_VERSION_MAJOR >= 5 #include -#endif namespace esphome { namespace ota { @@ -24,7 +21,6 @@ OTAResponseTypes IDFOTABackend::begin(size_t image_size) { #if CONFIG_ESP_TASK_WDT_TIMEOUT_S < 15 // The following function takes longer than the 5 seconds timeout of WDT -#if ESP_IDF_VERSION_MAJOR >= 5 esp_task_wdt_config_t wdtc; wdtc.idle_core_mask = 0; #if CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0 @@ -36,21 +32,14 @@ OTAResponseTypes IDFOTABackend::begin(size_t image_size) { wdtc.timeout_ms = 15000; wdtc.trigger_panic = false; esp_task_wdt_reconfigure(&wdtc); -#else - esp_task_wdt_init(15, false); -#endif #endif esp_err_t err = esp_ota_begin(this->partition_, image_size, &this->update_handle_); #if CONFIG_ESP_TASK_WDT_TIMEOUT_S < 15 // Set the WDT back to the configured timeout -#if ESP_IDF_VERSION_MAJOR >= 5 wdtc.timeout_ms = CONFIG_ESP_TASK_WDT_TIMEOUT_S * 1000; esp_task_wdt_reconfigure(&wdtc); -#else - esp_task_wdt_init(CONFIG_ESP_TASK_WDT_TIMEOUT_S, false); -#endif #endif if (err != ESP_OK) { @@ -67,7 +56,10 @@ OTAResponseTypes IDFOTABackend::begin(size_t image_size) { return OTA_RESPONSE_OK; } -void IDFOTABackend::set_update_md5(const char *expected_md5) { memcpy(this->expected_bin_md5_, expected_md5, 32); } +void IDFOTABackend::set_update_md5(const char *expected_md5) { + memcpy(this->expected_bin_md5_, expected_md5, 32); + this->md5_set_ = true; +} OTAResponseTypes IDFOTABackend::write(uint8_t *data, size_t len) { esp_err_t err = esp_ota_write(this->update_handle_, data, len); @@ -84,10 +76,12 @@ OTAResponseTypes IDFOTABackend::write(uint8_t *data, size_t len) { } OTAResponseTypes IDFOTABackend::end() { - this->md5_.calculate(); - if (!this->md5_.equals_hex(this->expected_bin_md5_)) { - this->abort(); - return OTA_RESPONSE_ERROR_MD5_MISMATCH; + if (this->md5_set_) { + this->md5_.calculate(); + if (!this->md5_.equals_hex(this->expected_bin_md5_)) { + this->abort(); + return OTA_RESPONSE_ERROR_MD5_MISMATCH; + } } esp_err_t err = esp_ota_end(this->update_handle_); this->update_handle_ = 0; diff --git a/esphome/components/ota/ota_backend_esp_idf.h b/esphome/components/ota/ota_backend_esp_idf.h index ed66d9b970..6e93982131 100644 --- a/esphome/components/ota/ota_backend_esp_idf.h +++ b/esphome/components/ota/ota_backend_esp_idf.h @@ -24,6 +24,7 @@ class IDFOTABackend : public OTABackend { const esp_partition_t *partition_; md5::MD5Digest md5_{}; char expected_bin_md5_[32]; + bool md5_set_{false}; }; } // namespace ota diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index 08ae798282..0db7841db2 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -63,6 +63,7 @@ BASE_SCHEMA = cv.All( cv.Schema( { cv.Required(CONF_URL): cv.url, + cv.Optional(CONF_PATH): cv.string, cv.Optional(CONF_USERNAME): cv.string, cv.Optional(CONF_PASSWORD): cv.string, cv.Exclusive(CONF_FILE, CONF_FILES): validate_yaml_filename, @@ -74,7 +75,7 @@ BASE_SCHEMA = cv.All( { cv.Required(CONF_PATH): validate_yaml_filename, cv.Optional(CONF_VARS, default={}): cv.Schema( - {cv.string: cv.string} + {cv.string: object} ), } ), @@ -116,6 +117,9 @@ def _process_base_package(config: dict) -> dict: ) files = [] + if base_path := config.get(CONF_PATH): + repo_dir = repo_dir / base_path + for file in config[CONF_FILES]: if isinstance(file, str): files.append({CONF_PATH: file, CONF_VARS: {}}) @@ -148,7 +152,6 @@ def _process_base_package(config: dict) -> dict: raise cv.Invalid( f"Current ESPHome Version is too old to use this package: {ESPHOME_VERSION} < {min_version}" ) - vars = {k: str(v) for k, v in vars.items()} new_yaml = yaml_util.substitute_vars(new_yaml, vars) packages[f"{filename}{idx}"] = new_yaml except EsphomeError as e: diff --git a/esphome/components/packet_transport/binary_sensor.py b/esphome/components/packet_transport/binary_sensor.py index 076e37e6bb..09bbf91c99 100644 --- a/esphome/components/packet_transport/binary_sensor.py +++ b/esphome/components/packet_transport/binary_sensor.py @@ -1,19 +1,76 @@ import esphome.codegen as cg from esphome.components import binary_sensor -from esphome.const import CONF_ID +import esphome.config_validation as cv +from esphome.const import ( + CONF_DATA, + CONF_ID, + CONF_NAME, + CONF_STATUS, + CONF_TYPE, + DEVICE_CLASS_CONNECTIVITY, + ENTITY_CATEGORY_DIAGNOSTIC, +) +import esphome.final_validate as fv from . import ( + CONF_ENCRYPTION, + CONF_PING_PONG_ENABLE, CONF_PROVIDER, + CONF_PROVIDERS, CONF_REMOTE_ID, CONF_TRANSPORT_ID, + PacketTransport, packet_transport_sensor_schema, + provider_name_validate, ) -CONFIG_SCHEMA = packet_transport_sensor_schema(binary_sensor.binary_sensor_schema()) +STATUS_SENSOR_SCHEMA = binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_CONNECTIVITY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, +).extend( + { + cv.GenerateID(CONF_TRANSPORT_ID): cv.use_id(PacketTransport), + cv.Required(CONF_PROVIDER): provider_name_validate, + } +) + +CONFIG_SCHEMA = cv.typed_schema( + { + CONF_DATA: packet_transport_sensor_schema(binary_sensor.binary_sensor_schema()), + CONF_STATUS: STATUS_SENSOR_SCHEMA, + }, + key=CONF_TYPE, + default_type=CONF_DATA, +) + + +def _final_validate(config): + if config[CONF_TYPE] != CONF_STATUS: + # Only run this validation if a status sensor is being configured + return config + full_config = fv.full_config.get() + transport_path = full_config.get_path_for_id(config[CONF_TRANSPORT_ID])[:-1] + transport_config = full_config.get_config_for_path(transport_path) + if transport_config[CONF_PING_PONG_ENABLE] and any( + CONF_ENCRYPTION in p + for p in transport_config[CONF_PROVIDERS] + if p[CONF_NAME] == config[CONF_PROVIDER] + ): + return config + raise cv.Invalid( + "Status sensor requires ping-pong to be enabled and the nominated provider to use encryption." + ) + + +FINAL_VALIDATE_SCHEMA = _final_validate async def to_code(config): var = await binary_sensor.new_binary_sensor(config) comp = await cg.get_variable(config[CONF_TRANSPORT_ID]) - remote_id = str(config.get(CONF_REMOTE_ID) or config.get(CONF_ID)) - cg.add(comp.add_remote_binary_sensor(config[CONF_PROVIDER], remote_id, var)) + if config[CONF_TYPE] == CONF_STATUS: + cg.add(comp.set_provider_status_sensor(config[CONF_PROVIDER], var)) + cg.add_define("USE_STATUS_SENSOR") + else: # CONF_DATA is default + remote_id = str(config.get(CONF_REMOTE_ID) or config.get(CONF_ID)) + cg.add(comp.add_remote_binary_sensor(config[CONF_PROVIDER], remote_id, var)) diff --git a/esphome/components/packet_transport/packet_transport.cpp b/esphome/components/packet_transport/packet_transport.cpp index 5c721002b0..6684d43ff7 100644 --- a/esphome/components/packet_transport/packet_transport.cpp +++ b/esphome/components/packet_transport/packet_transport.cpp @@ -317,8 +317,37 @@ void PacketTransport::update() { auto now = millis() / 1000; if (this->last_key_time_ + this->ping_pong_recyle_time_ < now) { this->resend_ping_key_ = this->ping_pong_enable_; + ESP_LOGV(TAG, "Ping request, age %u", now - this->last_key_time_); this->last_key_time_ = now; } + for (const auto &provider : this->providers_) { + uint32_t key_response_age = now - provider.second.last_key_response_time; + if (key_response_age > (this->ping_pong_recyle_time_ * 2u)) { +#ifdef USE_STATUS_SENSOR + if (provider.second.status_sensor != nullptr && provider.second.status_sensor->state) { + ESP_LOGI(TAG, "Ping status for %s timeout at %u with age %u", provider.first.c_str(), now, key_response_age); + provider.second.status_sensor->publish_state(false); + } +#endif +#ifdef USE_SENSOR + for (auto &sensor : this->remote_sensors_[provider.first]) { + sensor.second->publish_state(NAN); + } +#endif +#ifdef USE_BINARY_SENSOR + for (auto &sensor : this->remote_binary_sensors_[provider.first]) { + sensor.second->invalidate_state(); + } +#endif + } else { +#ifdef USE_STATUS_SENSOR + if (provider.second.status_sensor != nullptr && !provider.second.status_sensor->state) { + ESP_LOGI(TAG, "Ping status for %s restored at %u with age %u", provider.first.c_str(), now, key_response_age); + provider.second.status_sensor->publish_state(true); + } +#endif + } + } } void PacketTransport::add_key_(const char *name, uint32_t key) { @@ -437,7 +466,8 @@ void PacketTransport::process_(const std::vector &data) { if (decoder.decode(PING_KEY, key) == DECODE_OK) { if (key == this->ping_key_) { ping_key_seen = true; - ESP_LOGV(TAG, "Found good ping key %X", (unsigned) key); + provider.last_key_response_time = millis() / 1000; + ESP_LOGV(TAG, "Found good ping key %X at timestamp %" PRIu32, (unsigned) key, provider.last_key_response_time); } else { ESP_LOGV(TAG, "Unknown ping key %X", (unsigned) key); } diff --git a/esphome/components/packet_transport/packet_transport.h b/esphome/components/packet_transport/packet_transport.h index 34edb82963..a2370e9749 100644 --- a/esphome/components/packet_transport/packet_transport.h +++ b/esphome/components/packet_transport/packet_transport.h @@ -8,7 +8,7 @@ #ifdef USE_BINARY_SENSOR #include "esphome/components/binary_sensor/binary_sensor.h" #endif -# + #include #include @@ -27,6 +27,10 @@ struct Provider { std::vector encryption_key; const char *name; uint32_t last_code[2]; + uint32_t last_key_response_time; +#ifdef USE_STATUS_SENSOR + binary_sensor::BinarySensor *status_sensor{nullptr}; +#endif }; #ifdef USE_SENSOR @@ -75,10 +79,7 @@ class PacketTransport : public PollingComponent { void add_provider(const char *hostname) { if (this->providers_.count(hostname) == 0) { - Provider provider; - provider.encryption_key = std::vector{}; - provider.last_code[0] = 0; - provider.last_code[1] = 0; + Provider provider{}; provider.name = hostname; this->providers_[hostname] = provider; #ifdef USE_SENSOR @@ -97,6 +98,11 @@ class PacketTransport : public PollingComponent { void set_provider_encryption(const char *name, std::vector key) { this->providers_[name].encryption_key = std::move(key); } +#ifdef USE_STATUS_SENSOR + void set_provider_status_sensor(const char *name, binary_sensor::BinarySensor *sensor) { + this->providers_[name].status_sensor = sensor; + } +#endif void set_platform_name(const char *name) { this->platform_name_ = name; } protected: diff --git a/esphome/components/pcf8574/__init__.py b/esphome/components/pcf8574/__init__.py index 64bef86443..ff7c314bcd 100644 --- a/esphome/components/pcf8574/__init__.py +++ b/esphome/components/pcf8574/__init__.py @@ -53,7 +53,7 @@ PCF8574_PIN_SCHEMA = pins.gpio_base_schema( cv.int_range(min=0, max=17), modes=[CONF_INPUT, CONF_OUTPUT], mode_validator=validate_mode, - invertable=True, + invertible=True, ).extend( { cv.Required(CONF_PCF8574): cv.use_id(PCF8574Component), diff --git a/esphome/components/pi4ioe5v6408/__init__.py b/esphome/components/pi4ioe5v6408/__init__.py new file mode 100644 index 0000000000..c64f923823 --- /dev/null +++ b/esphome/components/pi4ioe5v6408/__init__.py @@ -0,0 +1,84 @@ +from esphome import pins +import esphome.codegen as cg +from esphome.components import i2c +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + CONF_INPUT, + CONF_INVERTED, + CONF_MODE, + CONF_NUMBER, + CONF_OUTPUT, + CONF_PULLDOWN, + CONF_PULLUP, + CONF_RESET, +) + +AUTO_LOAD = ["gpio_expander"] +CODEOWNERS = ["@jesserockz"] +DEPENDENCIES = ["i2c"] +MULTI_CONF = True + + +pi4ioe5v6408_ns = cg.esphome_ns.namespace("pi4ioe5v6408") +PI4IOE5V6408Component = pi4ioe5v6408_ns.class_( + "PI4IOE5V6408Component", cg.Component, i2c.I2CDevice +) +PI4IOE5V6408GPIOPin = pi4ioe5v6408_ns.class_("PI4IOE5V6408GPIOPin", cg.GPIOPin) + +CONF_PI4IOE5V6408 = "pi4ioe5v6408" + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(PI4IOE5V6408Component), + cv.Optional(CONF_RESET, default=True): cv.boolean, + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x43)) +) + + +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) + + cg.add(var.set_reset(config[CONF_RESET])) + + +def validate_mode(value): + if not (value[CONF_INPUT] or value[CONF_OUTPUT]): + raise cv.Invalid("Mode must be either input or output") + if value[CONF_INPUT] and value[CONF_OUTPUT]: + raise cv.Invalid("Mode must be either input or output") + return value + + +PI4IOE5V6408_PIN_SCHEMA = pins.gpio_base_schema( + PI4IOE5V6408GPIOPin, + cv.int_range(min=0, max=7), + modes=[ + CONF_INPUT, + CONF_OUTPUT, + CONF_PULLUP, + CONF_PULLDOWN, + ], + mode_validator=validate_mode, +).extend( + { + cv.Required(CONF_PI4IOE5V6408): cv.use_id(PI4IOE5V6408Component), + } +) + + +@pins.PIN_SCHEMA_REGISTRY.register(CONF_PI4IOE5V6408, PI4IOE5V6408_PIN_SCHEMA) +async def pi4ioe5v6408_pin_schema(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_parented(var, config[CONF_PI4IOE5V6408]) + + cg.add(var.set_pin(config[CONF_NUMBER])) + cg.add(var.set_inverted(config[CONF_INVERTED])) + cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) + return var diff --git a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp new file mode 100644 index 0000000000..55b8edffc8 --- /dev/null +++ b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp @@ -0,0 +1,171 @@ +#include "pi4ioe5v6408.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pi4ioe5v6408 { + +static const uint8_t PI4IOE5V6408_REGISTER_DEVICE_ID = 0x01; +static const uint8_t PI4IOE5V6408_REGISTER_IO_DIR = 0x03; +static const uint8_t PI4IOE5V6408_REGISTER_OUT_SET = 0x05; +static const uint8_t PI4IOE5V6408_REGISTER_OUT_HIGH_IMPEDENCE = 0x07; +static const uint8_t PI4IOE5V6408_REGISTER_IN_DEFAULT_STATE = 0x09; +static const uint8_t PI4IOE5V6408_REGISTER_PULL_ENABLE = 0x0B; +static const uint8_t PI4IOE5V6408_REGISTER_PULL_SELECT = 0x0D; +static const uint8_t PI4IOE5V6408_REGISTER_IN_STATE = 0x0F; +static const uint8_t PI4IOE5V6408_REGISTER_INTERRUPT_ENABLE_MASK = 0x11; +static const uint8_t PI4IOE5V6408_REGISTER_INTERRUPT_STATUS = 0x13; + +static const char *const TAG = "pi4ioe5v6408"; + +void PI4IOE5V6408Component::setup() { + ESP_LOGCONFIG(TAG, "Running setup"); + if (this->reset_) { + this->reg(PI4IOE5V6408_REGISTER_DEVICE_ID) |= 0b00000001; + this->reg(PI4IOE5V6408_REGISTER_OUT_HIGH_IMPEDENCE) = 0b00000000; + } else { + if (!this->read_gpio_modes_()) { + this->mark_failed(); + ESP_LOGE(TAG, "Failed to read GPIO modes"); + return; + } + if (!this->read_gpio_outputs_()) { + this->mark_failed(); + ESP_LOGE(TAG, "Failed to read GPIO outputs"); + return; + } + } +} +void PI4IOE5V6408Component::dump_config() { + ESP_LOGCONFIG(TAG, "PI4IOE5V6408:"); + LOG_I2C_DEVICE(this) + if (this->is_failed()) { + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); + } +} +void PI4IOE5V6408Component::pin_mode(uint8_t pin, gpio::Flags flags) { + if (flags & gpio::FLAG_OUTPUT) { + // Set mode mask bit + this->mode_mask_ |= 1 << pin; + } else if (flags & gpio::FLAG_INPUT) { + // Clear mode mask bit + this->mode_mask_ &= ~(1 << pin); + if (flags & gpio::FLAG_PULLUP) { + this->pull_up_down_mask_ |= 1 << pin; + this->pull_enable_mask_ |= 1 << pin; + } else if (flags & gpio::FLAG_PULLDOWN) { + this->pull_up_down_mask_ &= ~(1 << pin); + this->pull_enable_mask_ |= 1 << pin; + } + } + // Write GPIO to enable input mode + this->write_gpio_modes_(); +} + +void PI4IOE5V6408Component::loop() { this->reset_pin_cache_(); } + +bool PI4IOE5V6408Component::read_gpio_outputs_() { + if (this->is_failed()) + return false; + + uint8_t data; + if (!this->read_byte(PI4IOE5V6408_REGISTER_OUT_SET, &data)) { + this->status_set_warning("Failed to read output register"); + return false; + } + this->output_mask_ = data; + this->status_clear_warning(); + return true; +} + +bool PI4IOE5V6408Component::read_gpio_modes_() { + if (this->is_failed()) + return false; + + uint8_t data; + if (!this->read_byte(PI4IOE5V6408_REGISTER_IO_DIR, &data)) { + this->status_set_warning("Failed to read GPIO modes"); + return false; + } +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + ESP_LOGV(TAG, "Read GPIO modes: 0b" BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(data)); +#endif + this->mode_mask_ = data; + this->status_clear_warning(); + return true; +} + +bool PI4IOE5V6408Component::digital_read_hw(uint8_t pin) { + if (this->is_failed()) + return false; + + uint8_t data; + if (!this->read_byte(PI4IOE5V6408_REGISTER_IN_STATE, &data)) { + this->status_set_warning("Failed to read GPIO state"); + return false; + } + this->input_mask_ = data; + this->status_clear_warning(); + return true; +} + +void PI4IOE5V6408Component::digital_write_hw(uint8_t pin, bool value) { + if (this->is_failed()) + return; + + if (value) { + this->output_mask_ |= (1 << pin); + } else { + this->output_mask_ &= ~(1 << pin); + } + if (!this->write_byte(PI4IOE5V6408_REGISTER_OUT_SET, this->output_mask_)) { + this->status_set_warning("Failed to write output register"); + return; + } +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + ESP_LOGV(TAG, "Wrote GPIO output: 0b" BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(this->output_mask_)); +#endif + this->status_clear_warning(); +} + +bool PI4IOE5V6408Component::write_gpio_modes_() { + if (this->is_failed()) + return false; + + if (!this->write_byte(PI4IOE5V6408_REGISTER_IO_DIR, this->mode_mask_)) { + this->status_set_warning("Failed to write GPIO modes"); + return false; + } + if (!this->write_byte(PI4IOE5V6408_REGISTER_PULL_SELECT, this->pull_up_down_mask_)) { + this->status_set_warning("Failed to write GPIO pullup/pulldown"); + return false; + } + if (!this->write_byte(PI4IOE5V6408_REGISTER_PULL_ENABLE, this->pull_enable_mask_)) { + this->status_set_warning("Failed to write GPIO pull enable"); + return false; + } +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + ESP_LOGV(TAG, + "Wrote GPIO modes: 0b" BYTE_TO_BINARY_PATTERN "\n" + "Wrote GPIO pullup/pulldown: 0b" BYTE_TO_BINARY_PATTERN "\n" + "Wrote GPIO pull enable: 0b" BYTE_TO_BINARY_PATTERN, + BYTE_TO_BINARY(this->mode_mask_), BYTE_TO_BINARY(this->pull_up_down_mask_), + BYTE_TO_BINARY(this->pull_enable_mask_)); +#endif + this->status_clear_warning(); + return true; +} + +bool PI4IOE5V6408Component::digital_read_cache(uint8_t pin) { return (this->input_mask_ & (1 << pin)); } + +float PI4IOE5V6408Component::get_setup_priority() const { return setup_priority::IO; } + +void PI4IOE5V6408GPIOPin::setup() { this->pin_mode(this->flags_); } +void PI4IOE5V6408GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } +bool PI4IOE5V6408GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } +void PI4IOE5V6408GPIOPin::digital_write(bool value) { + this->parent_->digital_write(this->pin_, value != this->inverted_); +} +std::string PI4IOE5V6408GPIOPin::dump_summary() const { return str_sprintf("%u via PI4IOE5V6408", this->pin_); } + +} // namespace pi4ioe5v6408 +} // namespace esphome diff --git a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.h b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.h new file mode 100644 index 0000000000..82b3076fab --- /dev/null +++ b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.h @@ -0,0 +1,70 @@ +#pragma once + +#include "esphome/components/gpio_expander/cached_gpio.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace pi4ioe5v6408 { +class PI4IOE5V6408Component : public Component, + public i2c::I2CDevice, + public gpio_expander::CachedGpioExpander { + public: + PI4IOE5V6408Component() = default; + + void setup() override; + void pin_mode(uint8_t pin, gpio::Flags flags); + + float get_setup_priority() const override; + void dump_config() override; + void loop() override; + + /// Indicate if the component should reset the state during setup + void set_reset(bool reset) { this->reset_ = reset; } + + protected: + bool digital_read_hw(uint8_t pin) override; + bool digital_read_cache(uint8_t pin) override; + void digital_write_hw(uint8_t pin, bool value) override; + + /// Mask for the pin mode - 1 means output, 0 means input + uint8_t mode_mask_{0x00}; + /// The mask to write as output state - 1 means HIGH, 0 means LOW + uint8_t output_mask_{0x00}; + /// The state read in digital_read_hw - 1 means HIGH, 0 means LOW + uint8_t input_mask_{0x00}; + /// The mask to write as input buffer state - 1 means enabled, 0 means disabled + uint8_t pull_enable_mask_{0x00}; + /// The mask to write as pullup state - 1 means pullup, 0 means pulldown + uint8_t pull_up_down_mask_{0x00}; + + bool reset_{true}; + + bool read_gpio_modes_(); + bool write_gpio_modes_(); + bool read_gpio_outputs_(); +}; + +class PI4IOE5V6408GPIOPin : public GPIOPin, public Parented { + public: + void setup() override; + void pin_mode(gpio::Flags flags) override; + bool digital_read() override; + void digital_write(bool value) override; + std::string dump_summary() const override; + + void set_pin(uint8_t pin) { this->pin_ = pin; } + void set_inverted(bool inverted) { this->inverted_ = inverted; } + void set_flags(gpio::Flags flags) { this->flags_ = flags; } + + gpio::Flags get_flags() const override { return this->flags_; } + + protected: + uint8_t pin_; + bool inverted_; + gpio::Flags flags_; +}; + +} // namespace pi4ioe5v6408 +} // namespace esphome diff --git a/esphome/components/pmsa003i/pmsa003i.h b/esphome/components/pmsa003i/pmsa003i.h index 59f39a7314..cd106704a6 100644 --- a/esphome/components/pmsa003i/pmsa003i.h +++ b/esphome/components/pmsa003i/pmsa003i.h @@ -32,7 +32,6 @@ class PMSA003IComponent : public PollingComponent, public i2c::I2CDevice { void setup() override; void dump_config() override; void update() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_standard_units(bool standard_units) { this->standard_units_ = standard_units; } diff --git a/esphome/components/pmsx003/pmsx003.h b/esphome/components/pmsx003/pmsx003.h index e422d4165b..ba607b4487 100644 --- a/esphome/components/pmsx003/pmsx003.h +++ b/esphome/components/pmsx003/pmsx003.h @@ -31,7 +31,6 @@ enum PMSX003State { class PMSX003Component : public uart::UARTDevice, public Component { public: PMSX003Component() = default; - float get_setup_priority() const override { return setup_priority::DATA; } void dump_config() override; void loop() override; diff --git a/esphome/components/pn7150/pn7150.cpp b/esphome/components/pn7150/pn7150.cpp index 971ddd23cb..f827bd151a 100644 --- a/esphome/components/pn7150/pn7150.cpp +++ b/esphome/components/pn7150/pn7150.cpp @@ -584,7 +584,7 @@ void PN7150::nci_fsm_transition_() { } else { this->nci_fsm_set_state_(NCIState::NFCC_INIT); } - // fall through + [[fallthrough]]; case NCIState::NFCC_INIT: if (this->init_core_() != nfc::STATUS_OK) { @@ -594,7 +594,7 @@ void PN7150::nci_fsm_transition_() { } else { this->nci_fsm_set_state_(NCIState::NFCC_CONFIG); } - // fall through + [[fallthrough]]; case NCIState::NFCC_CONFIG: if (this->send_init_config_() != nfc::STATUS_OK) { @@ -605,7 +605,7 @@ void PN7150::nci_fsm_transition_() { this->config_refresh_pending_ = false; this->nci_fsm_set_state_(NCIState::NFCC_SET_DISCOVER_MAP); } - // fall through + [[fallthrough]]; case NCIState::NFCC_SET_DISCOVER_MAP: if (this->set_discover_map_() != nfc::STATUS_OK) { @@ -615,7 +615,7 @@ void PN7150::nci_fsm_transition_() { } else { this->nci_fsm_set_state_(NCIState::NFCC_SET_LISTEN_MODE_ROUTING); } - // fall through + [[fallthrough]]; case NCIState::NFCC_SET_LISTEN_MODE_ROUTING: if (this->set_listen_mode_routing_() != nfc::STATUS_OK) { @@ -625,7 +625,7 @@ void PN7150::nci_fsm_transition_() { } else { this->nci_fsm_set_state_(NCIState::RFST_IDLE); } - // fall through + [[fallthrough]]; case NCIState::RFST_IDLE: if (this->nci_state_error_ == NCIState::RFST_DISCOVERY) { @@ -650,14 +650,14 @@ void PN7150::nci_fsm_transition_() { case NCIState::RFST_W4_HOST_SELECT: select_endpoint_(); - // fall through + [[fallthrough]]; // All cases below are waiting for NOTIFICATION messages case NCIState::RFST_DISCOVERY: if (this->config_refresh_pending_) { this->refresh_core_config_(); } - // fall through + [[fallthrough]]; case NCIState::RFST_LISTEN_ACTIVE: case NCIState::RFST_LISTEN_SLEEP: diff --git a/esphome/components/pn7150/pn7150.h b/esphome/components/pn7150/pn7150.h index 87af7d629b..42cd7a6ef7 100644 --- a/esphome/components/pn7150/pn7150.h +++ b/esphome/components/pn7150/pn7150.h @@ -146,7 +146,6 @@ class PN7150 : public nfc::Nfcc, public Component { public: void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void loop() override; void set_irq_pin(GPIOPin *irq_pin) { this->irq_pin_ = irq_pin; } diff --git a/esphome/components/pn7160/pn7160.cpp b/esphome/components/pn7160/pn7160.cpp index 2a1de20657..a8edfadd8e 100644 --- a/esphome/components/pn7160/pn7160.cpp +++ b/esphome/components/pn7160/pn7160.cpp @@ -609,7 +609,7 @@ void PN7160::nci_fsm_transition_() { } else { this->nci_fsm_set_state_(NCIState::NFCC_INIT); } - // fall through + [[fallthrough]]; case NCIState::NFCC_INIT: if (this->init_core_() != nfc::STATUS_OK) { @@ -619,7 +619,7 @@ void PN7160::nci_fsm_transition_() { } else { this->nci_fsm_set_state_(NCIState::NFCC_CONFIG); } - // fall through + [[fallthrough]]; case NCIState::NFCC_CONFIG: if (this->send_init_config_() != nfc::STATUS_OK) { @@ -630,7 +630,7 @@ void PN7160::nci_fsm_transition_() { this->config_refresh_pending_ = false; this->nci_fsm_set_state_(NCIState::NFCC_SET_DISCOVER_MAP); } - // fall through + [[fallthrough]]; case NCIState::NFCC_SET_DISCOVER_MAP: if (this->set_discover_map_() != nfc::STATUS_OK) { @@ -640,7 +640,7 @@ void PN7160::nci_fsm_transition_() { } else { this->nci_fsm_set_state_(NCIState::NFCC_SET_LISTEN_MODE_ROUTING); } - // fall through + [[fallthrough]]; case NCIState::NFCC_SET_LISTEN_MODE_ROUTING: if (this->set_listen_mode_routing_() != nfc::STATUS_OK) { @@ -650,7 +650,7 @@ void PN7160::nci_fsm_transition_() { } else { this->nci_fsm_set_state_(NCIState::RFST_IDLE); } - // fall through + [[fallthrough]]; case NCIState::RFST_IDLE: if (this->nci_state_error_ == NCIState::RFST_DISCOVERY) { @@ -675,14 +675,14 @@ void PN7160::nci_fsm_transition_() { case NCIState::RFST_W4_HOST_SELECT: select_endpoint_(); - // fall through + [[fallthrough]]; // All cases below are waiting for NOTIFICATION messages case NCIState::RFST_DISCOVERY: if (this->config_refresh_pending_) { this->refresh_core_config_(); } - // fall through + [[fallthrough]]; case NCIState::RFST_LISTEN_ACTIVE: case NCIState::RFST_LISTEN_SLEEP: diff --git a/esphome/components/pn7160/pn7160.h b/esphome/components/pn7160/pn7160.h index ff8a492b7b..fc00296a71 100644 --- a/esphome/components/pn7160/pn7160.h +++ b/esphome/components/pn7160/pn7160.h @@ -161,7 +161,6 @@ class PN7160 : public nfc::Nfcc, public Component { public: void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void loop() override; void set_dwl_req_pin(GPIOPin *dwl_req_pin) { this->dwl_req_pin_ = dwl_req_pin; } diff --git a/esphome/components/preferences/syncer.h b/esphome/components/preferences/syncer.h index 8976a1fe15..b6b422d4ba 100644 --- a/esphome/components/preferences/syncer.h +++ b/esphome/components/preferences/syncer.h @@ -12,6 +12,8 @@ class IntervalSyncer : public Component { void setup() override { if (this->write_interval_ != 0) { set_interval(this->write_interval_, []() { global_preferences->sync(); }); + // When using interval-based syncing, we don't need the loop + this->disable_loop(); } } void loop() override { diff --git a/esphome/components/prometheus/prometheus_handler.h b/esphome/components/prometheus/prometheus_handler.h index bdc3d971ce..c4598f44b0 100644 --- a/esphome/components/prometheus/prometheus_handler.h +++ b/esphome/components/prometheus/prometheus_handler.h @@ -40,7 +40,7 @@ class PrometheusHandler : public AsyncWebHandler, public Component { */ void add_label_name(EntityBase *obj, const std::string &value) { relabel_map_name_.insert({obj, value}); } - bool canHandle(AsyncWebServerRequest *request) override { + bool canHandle(AsyncWebServerRequest *request) const override { if (request->method() == HTTP_GET) { if (request->url() == "/metrics") return true; diff --git a/esphome/components/psram/psram.cpp b/esphome/components/psram/psram.cpp index 162543545e..6c110a577d 100644 --- a/esphome/components/psram/psram.cpp +++ b/esphome/components/psram/psram.cpp @@ -2,9 +2,7 @@ #ifdef USE_ESP32 #include "psram.h" #include -#if defined(USE_ESP_IDF) && ESP_IDF_VERSION_MAJOR >= 5 #include -#endif // USE_ESP_IDF #include "esphome/core/log.h" @@ -16,7 +14,6 @@ static const char *const TAG = "psram"; void PsramComponent::dump_config() { ESP_LOGCONFIG(TAG, "PSRAM:"); -#if defined(USE_ESP_IDF) && ESP_IDF_VERSION_MAJOR >= 5 bool available = esp_psram_is_initialized(); ESP_LOGCONFIG(TAG, " Available: %s", YESNO(available)); @@ -26,23 +23,6 @@ void PsramComponent::dump_config() { ESP_LOGCONFIG(TAG, " ECC enabled: YES"); #endif } -#else - // Technically this can be false if the PSRAM is full, but heap_caps_get_total_size() isn't always available, and it's - // very unlikely for the PSRAM to be full. - bool available = heap_caps_get_free_size(MALLOC_CAP_SPIRAM) > 0; - ESP_LOGCONFIG(TAG, " Available: %s", YESNO(available)); - - if (available) { - const size_t psram_total_size_bytes = heap_caps_get_total_size(MALLOC_CAP_SPIRAM); - const float psram_total_size_kb = psram_total_size_bytes / 1024.0f; - - if (abs(std::round(psram_total_size_kb) - psram_total_size_kb) < 0.05f) { - ESP_LOGCONFIG(TAG, " Size: %.0f KB", psram_total_size_kb); - } else { - ESP_LOGCONFIG(TAG, " Size: %zu bytes", psram_total_size_bytes); - } - } -#endif // USE_ESP_IDF } } // namespace psram diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.h b/esphome/components/pulse_counter/pulse_counter_sensor.h index cea9fa7bf9..5ba59cca2a 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.h +++ b/esphome/components/pulse_counter/pulse_counter_sensor.h @@ -76,7 +76,6 @@ class PulseCounterSensor : public sensor::Sensor, public PollingComponent { /// Unit of measurement is "pulses/min". void setup() override; void update() override; - float get_setup_priority() const override { return setup_priority::DATA; } void dump_config() override; protected: diff --git a/esphome/components/pulse_meter/pulse_meter_sensor.cpp b/esphome/components/pulse_meter/pulse_meter_sensor.cpp index b82cb7a15c..9a7630a7be 100644 --- a/esphome/components/pulse_meter/pulse_meter_sensor.cpp +++ b/esphome/components/pulse_meter/pulse_meter_sensor.cpp @@ -31,6 +31,10 @@ void PulseMeterSensor::setup() { this->pulse_state_.latched_ = this->last_pin_val_; this->pin_->attach_interrupt(PulseMeterSensor::pulse_intr, this, gpio::INTERRUPT_ANY_EDGE); } + + if (this->total_sensor_ != nullptr) { + this->total_sensor_->publish_state(this->total_pulses_); + } } void PulseMeterSensor::loop() { @@ -63,7 +67,7 @@ void PulseMeterSensor::loop() { // If an edge was peeked, repay the debt if (this->peeked_edge_ && this->get_->count_ > 0) { this->peeked_edge_ = false; - this->get_->count_--; + this->get_->count_--; // NOLINT(clang-diagnostic-deprecated-volatile) } // If there is an unprocessed edge, and filter_us_ has passed since, count this edge early @@ -71,7 +75,7 @@ void PulseMeterSensor::loop() { now - this->get_->last_rising_edge_us_ >= this->filter_us_) { this->peeked_edge_ = true; this->get_->last_detected_edge_us_ = this->get_->last_rising_edge_us_; - this->get_->count_++; + this->get_->count_++; // NOLINT(clang-diagnostic-deprecated-volatile) } // Check if we detected a pulse this loop @@ -146,7 +150,7 @@ void IRAM_ATTR PulseMeterSensor::edge_intr(PulseMeterSensor *sensor) { state.last_sent_edge_us_ = now; set.last_detected_edge_us_ = now; set.last_rising_edge_us_ = now; - set.count_++; + set.count_++; // NOLINT(clang-diagnostic-deprecated-volatile) } // This ISR is bound to rising edges, so the pin is high @@ -169,7 +173,7 @@ void IRAM_ATTR PulseMeterSensor::pulse_intr(PulseMeterSensor *sensor) { } else if (length && !state.latched_ && sensor->last_pin_val_) { // Long enough high edge state.latched_ = true; set.last_detected_edge_us_ = state.last_intr_; - set.count_++; + set.count_++; // NOLINT(clang-diagnostic-deprecated-volatile) } // Due to order of operations this includes diff --git a/esphome/components/pulse_width/pulse_width.h b/esphome/components/pulse_width/pulse_width.h index 822688ec88..c6b896988d 100644 --- a/esphome/components/pulse_width/pulse_width.h +++ b/esphome/components/pulse_width/pulse_width.h @@ -32,7 +32,6 @@ class PulseWidthSensor : public sensor::Sensor, public PollingComponent { void set_pin(InternalGPIOPin *pin) { pin_ = pin; } void setup() override { this->store_.setup(this->pin_); } void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void update() override; protected: diff --git a/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp b/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp index 74f63a9640..4b6c11b332 100644 --- a/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp +++ b/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp @@ -146,11 +146,7 @@ void PVVXDisplay::sync_time_() { } time.recalc_timestamp_utc(true); // calculate timestamp of local time uint8_t blk[5] = {}; -#if ESP_IDF_VERSION_MAJOR >= 5 ESP_LOGD(TAG, "[%s] Sync time with timestamp %" PRIu64 ".", this->parent_->address_str().c_str(), time.timestamp); -#else - ESP_LOGD(TAG, "[%s] Sync time with timestamp %lu.", this->parent_->address_str().c_str(), time.timestamp); -#endif blk[0] = 0x23; blk[1] = time.timestamp & 0xff; blk[2] = (time.timestamp >> 8) & 0xff; diff --git a/esphome/components/pvvx_mithermometer/display/pvvx_display.h b/esphome/components/pvvx_mithermometer/display/pvvx_display.h index dfeb49c49d..9739362024 100644 --- a/esphome/components/pvvx_mithermometer/display/pvvx_display.h +++ b/esphome/components/pvvx_mithermometer/display/pvvx_display.h @@ -39,8 +39,6 @@ class PVVXDisplay : public ble_client::BLEClientNode, public PollingComponent { void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } - void update() override; void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, diff --git a/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h index 99455a1663..9614a3c586 100644 --- a/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h +++ b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h @@ -25,7 +25,6 @@ class PVVXMiThermometer : public Component, public esp32_ble_tracker::ESPBTDevic bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } diff --git a/esphome/components/qr_code/__init__.py b/esphome/components/qr_code/__init__.py index 1c5e0471b0..6ff92b8a7f 100644 --- a/esphome/components/qr_code/__init__.py +++ b/esphome/components/qr_code/__init__.py @@ -21,21 +21,24 @@ ECC = { "HIGH": qrcodegen_Ecc.qrcodegen_Ecc_HIGH, } -CONFIG_SCHEMA = cv.Schema( - { - cv.Required(CONF_ID): cv.declare_id(QRCode), - cv.Required(CONF_VALUE): cv.string, - cv.Optional(CONF_ECC, default="LOW"): cv.enum(ECC, upper=True), - } +CONFIG_SCHEMA = cv.ensure_list( + cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(QRCode), + cv.Required(CONF_VALUE): cv.string, + cv.Optional(CONF_ECC, default="LOW"): cv.enum(ECC, upper=True), + } + ) ) async def to_code(config): cg.add_library("wjtje/qr-code-generator-library", "^1.7.0") - var = cg.new_Pvariable(config[CONF_ID]) - cg.add(var.set_value(config[CONF_VALUE])) - cg.add(var.set_ecc(ECC[config[CONF_ECC]])) - await cg.register_component(var, config) + for entry in config: + var = cg.new_Pvariable(entry[CONF_ID]) + cg.add(var.set_value(entry[CONF_VALUE])) + cg.add(var.set_ecc(ECC[entry[CONF_ECC]])) + await cg.register_component(var, entry) cg.add_define("USE_QR_CODE") diff --git a/esphome/components/qwiic_pir/qwiic_pir.h b/esphome/components/qwiic_pir/qwiic_pir.h index d58d67734f..797ded2cc6 100644 --- a/esphome/components/qwiic_pir/qwiic_pir.h +++ b/esphome/components/qwiic_pir/qwiic_pir.h @@ -36,7 +36,6 @@ class QwiicPIRComponent : public Component, public i2c::I2CDevice, public binary void loop() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_debounce_time(uint16_t debounce_time) { this->debounce_time_ = debounce_time; } void set_debounce_mode(DebounceMode mode) { this->debounce_mode_ = mode; } diff --git a/esphome/components/radon_eye_ble/radon_eye_listener.cpp b/esphome/components/radon_eye_ble/radon_eye_listener.cpp index a4c79db753..0c6165c691 100644 --- a/esphome/components/radon_eye_ble/radon_eye_listener.cpp +++ b/esphome/components/radon_eye_ble/radon_eye_listener.cpp @@ -17,7 +17,7 @@ bool RadonEyeListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device // Check if the device name starts with any of the prefixes if (std::any_of(prefixes.begin(), prefixes.end(), - [&](const std::string &prefix) { return device.get_name().rfind(prefix, 0) == 0; })) { + [&](const std::string &prefix) { return device.get_name().starts_with(prefix); })) { // Device found ESP_LOGD(TAG, "Found Radon Eye device Name: %s (MAC: %s)", device.get_name().c_str(), device.address_str().c_str()); diff --git a/esphome/components/rc522/rc522.h b/esphome/components/rc522/rc522.h index c6c5e119f0..437cea808b 100644 --- a/esphome/components/rc522/rc522.h +++ b/esphome/components/rc522/rc522.h @@ -19,7 +19,6 @@ class RC522 : public PollingComponent { void dump_config() override; void update() override; - float get_setup_priority() const override { return setup_priority::DATA; }; void loop() override; diff --git a/esphome/components/rdm6300/rdm6300.h b/esphome/components/rdm6300/rdm6300.h index 1a1a0c0cd6..24a808b62c 100644 --- a/esphome/components/rdm6300/rdm6300.h +++ b/esphome/components/rdm6300/rdm6300.h @@ -21,8 +21,6 @@ class RDM6300Component : public Component, public uart::UARTDevice { void register_card(RDM6300BinarySensor *obj) { this->cards_.push_back(obj); } void register_trigger(RDM6300Trigger *trig) { this->triggers_.push_back(trig); } - float get_setup_priority() const override { return setup_priority::DATA; } - protected: int8_t read_state_{-1}; uint8_t buffer_[6]{}; diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 836b98104b..fc824ef704 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -57,7 +57,7 @@ RemoteReceiverBinarySensorBase = ns.class_( RemoteReceiverTrigger = ns.class_( "RemoteReceiverTrigger", automation.Trigger, RemoteReceiverListener ) -RemoteTransmitterDumper = ns.class_("RemoteTransmitterDumper") +RemoteReceiverDumperBase = ns.class_("RemoteReceiverDumperBase") RemoteTransmittable = ns.class_("RemoteTransmittable") RemoteTransmitterActionBase = ns.class_( "RemoteTransmitterActionBase", RemoteTransmittable, automation.Action @@ -126,8 +126,10 @@ def register_trigger(name, type, data_type): return decorator -def register_dumper(name, type): - registerer = DUMPER_REGISTRY.register(name, type, {}) +def register_dumper(name, type, schema=None): + if schema is None: + schema = {} + registerer = DUMPER_REGISTRY.register(name, type, schema) def decorator(func): async def new_func(config, dumper_id): @@ -189,7 +191,7 @@ def declare_protocol(name): binary_sensor_ = ns.class_(f"{name}BinarySensor", RemoteReceiverBinarySensorBase) trigger = ns.class_(f"{name}Trigger", RemoteReceiverTrigger) action = ns.class_(f"{name}Action", RemoteTransmitterActionBase) - dumper = ns.class_(f"{name}Dumper", RemoteTransmitterDumper) + dumper = ns.class_(f"{name}Dumper", RemoteReceiverDumperBase) return data, binary_sensor_, trigger, action, dumper @@ -1405,7 +1407,7 @@ rc_switch_protocols = ns.RC_SWITCH_PROTOCOLS RCSwitchData = ns.struct("RCSwitchData") RCSwitchBase = ns.class_("RCSwitchBase") RCSwitchTrigger = ns.class_("RCSwitchTrigger", RemoteReceiverTrigger) -RCSwitchDumper = ns.class_("RCSwitchDumper", RemoteTransmitterDumper) +RCSwitchDumper = ns.class_("RCSwitchDumper", RemoteReceiverDumperBase) RCSwitchRawAction = ns.class_("RCSwitchRawAction", RemoteTransmitterActionBase) RCSwitchTypeAAction = ns.class_("RCSwitchTypeAAction", RemoteTransmitterActionBase) RCSwitchTypeBAction = ns.class_("RCSwitchTypeBAction", RemoteTransmitterActionBase) diff --git a/esphome/components/remote_base/remote_base.cpp b/esphome/components/remote_base/remote_base.cpp index 5dff2c6a38..34aba236b3 100644 --- a/esphome/components/remote_base/remote_base.cpp +++ b/esphome/components/remote_base/remote_base.cpp @@ -8,27 +8,6 @@ namespace remote_base { static const char *const TAG = "remote_base"; -#if defined(USE_ESP32) && ESP_IDF_VERSION_MAJOR < 5 -RemoteRMTChannel::RemoteRMTChannel(uint8_t mem_block_num) : mem_block_num_(mem_block_num) { - static rmt_channel_t next_rmt_channel = RMT_CHANNEL_0; - this->channel_ = next_rmt_channel; - next_rmt_channel = rmt_channel_t(int(next_rmt_channel) + mem_block_num); -} - -RemoteRMTChannel::RemoteRMTChannel(rmt_channel_t channel, uint8_t mem_block_num) - : channel_(channel), mem_block_num_(mem_block_num) {} - -void RemoteRMTChannel::config_rmt(rmt_config_t &rmt) { - if (rmt_channel_t(int(this->channel_) + this->mem_block_num_) > RMT_CHANNEL_MAX) { - this->mem_block_num_ = int(RMT_CHANNEL_MAX) - int(this->channel_); - ESP_LOGW(TAG, "Not enough RMT memory blocks available, reduced to %i blocks.", this->mem_block_num_); - } - rmt.channel = this->channel_; - rmt.clk_div = this->clock_divider_; - rmt.mem_block_num = this->mem_block_num_; -} -#endif - /* RemoteReceiveData */ bool RemoteReceiveData::peek_mark(uint32_t length, uint32_t offset) const { @@ -40,6 +19,22 @@ bool RemoteReceiveData::peek_mark(uint32_t length, uint32_t offset) const { return value >= 0 && lo <= value && value <= hi; } +bool RemoteReceiveData::peek_mark_at_least(uint32_t length, uint32_t offset) const { + if (!this->is_valid(offset)) + return false; + const int32_t value = this->peek(offset); + const int32_t lo = this->lower_bound_(length); + return value >= 0 && lo <= value; +} + +bool RemoteReceiveData::peek_mark_at_most(uint32_t length, uint32_t offset) const { + if (!this->is_valid(offset)) + return false; + const int32_t value = this->peek(offset); + const int32_t hi = this->upper_bound_(length); + return value >= 0 && value <= hi; +} + bool RemoteReceiveData::peek_space(uint32_t length, uint32_t offset) const { if (!this->is_valid(offset)) return false; @@ -57,6 +52,14 @@ bool RemoteReceiveData::peek_space_at_least(uint32_t length, uint32_t offset) co return value <= 0 && lo <= -value; } +bool RemoteReceiveData::peek_space_at_most(uint32_t length, uint32_t offset) const { + if (!this->is_valid(offset)) + return false; + const int32_t value = this->peek(offset); + const int32_t hi = this->upper_bound_(length); + return value <= 0 && -value <= hi; +} + bool RemoteReceiveData::expect_mark(uint32_t length) { if (!this->peek_mark(length)) return false; diff --git a/esphome/components/remote_base/remote_base.h b/esphome/components/remote_base/remote_base.h index 4131d080f5..b740ba8085 100644 --- a/esphome/components/remote_base/remote_base.h +++ b/esphome/components/remote_base/remote_base.h @@ -8,10 +8,6 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" -#if defined(USE_ESP32) && ESP_IDF_VERSION_MAJOR < 5 -#include -#endif - namespace esphome { namespace remote_base { @@ -57,8 +53,11 @@ class RemoteReceiveData { bool is_valid(uint32_t offset = 0) const { return this->index_ + offset < this->data_.size(); } int32_t peek(uint32_t offset = 0) const { return this->data_[this->index_ + offset]; } bool peek_mark(uint32_t length, uint32_t offset = 0) const; + bool peek_mark_at_least(uint32_t length, uint32_t offset = 0) const; + bool peek_mark_at_most(uint32_t length, uint32_t offset = 0) const; bool peek_space(uint32_t length, uint32_t offset = 0) const; bool peek_space_at_least(uint32_t length, uint32_t offset = 0) const; + bool peek_space_at_most(uint32_t length, uint32_t offset = 0) const; bool peek_item(uint32_t mark, uint32_t space, uint32_t offset = 0) const { return this->peek_space(space, offset + 1) && this->peek_mark(mark, offset); } @@ -112,43 +111,21 @@ class RemoteComponentBase { #ifdef USE_ESP32 class RemoteRMTChannel { public: -#if ESP_IDF_VERSION_MAJOR >= 5 void set_clock_resolution(uint32_t clock_resolution) { this->clock_resolution_ = clock_resolution; } void set_rmt_symbols(uint32_t rmt_symbols) { this->rmt_symbols_ = rmt_symbols; } -#else - explicit RemoteRMTChannel(uint8_t mem_block_num = 1); - explicit RemoteRMTChannel(rmt_channel_t channel, uint8_t mem_block_num = 1); - - void config_rmt(rmt_config_t &rmt); - void set_clock_divider(uint8_t clock_divider) { this->clock_divider_ = clock_divider; } -#endif protected: uint32_t from_microseconds_(uint32_t us) { -#if ESP_IDF_VERSION_MAJOR >= 5 const uint32_t ticks_per_ten_us = this->clock_resolution_ / 100000u; -#else - const uint32_t ticks_per_ten_us = 80000000u / this->clock_divider_ / 100000u; -#endif return us * ticks_per_ten_us / 10; } uint32_t to_microseconds_(uint32_t ticks) { -#if ESP_IDF_VERSION_MAJOR >= 5 const uint32_t ticks_per_ten_us = this->clock_resolution_ / 100000u; -#else - const uint32_t ticks_per_ten_us = 80000000u / this->clock_divider_ / 100000u; -#endif return (ticks * 10) / ticks_per_ten_us; } RemoteComponentBase *remote_base_; -#if ESP_IDF_VERSION_MAJOR >= 5 uint32_t clock_resolution_{1000000}; uint32_t rmt_symbols_; -#else - rmt_channel_t channel_{RMT_CHANNEL_0}; - uint8_t mem_block_num_; - uint8_t clock_divider_{80}; -#endif }; #endif diff --git a/esphome/components/remote_receiver/__init__.py b/esphome/components/remote_receiver/__init__.py index 6994eebd91..dffc088085 100644 --- a/esphome/components/remote_receiver/__init__.py +++ b/esphome/components/remote_receiver/__init__.py @@ -1,23 +1,22 @@ from esphome import pins import esphome.codegen as cg from esphome.components import esp32, esp32_rmt, remote_base +from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( CONF_BUFFER_SIZE, - CONF_CLOCK_DIVIDER, CONF_CLOCK_RESOLUTION, CONF_DUMP, CONF_FILTER, CONF_ID, CONF_IDLE, - CONF_MEMORY_BLOCKS, CONF_PIN, - CONF_RMT_CHANNEL, CONF_RMT_SYMBOLS, CONF_TOLERANCE, CONF_TYPE, CONF_USE_DMA, CONF_VALUE, + PlatformFramework, ) from esphome.core import CORE, TimePeriod @@ -97,55 +96,43 @@ CONFIG_SCHEMA = remote_base.validate_triggers( esp32="10000b", esp8266="1000b", bk72xx="1000b", + ln882x="1000b", rtl87xx="1000b", ): cv.validate_bytes, cv.Optional(CONF_FILTER, default="50us"): cv.All( cv.positive_time_period_microseconds, cv.Range(max=TimePeriod(microseconds=4294967295)), ), - cv.SplitDefault(CONF_CLOCK_DIVIDER, esp32_arduino=80): cv.All( - cv.only_on_esp32, - cv.only_with_arduino, - cv.int_range(min=1, max=255), - ), cv.Optional(CONF_CLOCK_RESOLUTION): cv.All( cv.only_on_esp32, - cv.only_with_esp_idf, esp32_rmt.validate_clock_resolution(), ), cv.Optional(CONF_IDLE, default="10ms"): cv.All( cv.positive_time_period_microseconds, cv.Range(max=TimePeriod(microseconds=4294967295)), ), - cv.SplitDefault(CONF_MEMORY_BLOCKS, esp32_arduino=3): cv.All( - cv.only_with_arduino, cv.int_range(min=1, max=8) - ), - cv.Optional(CONF_RMT_CHANNEL): cv.All( - cv.only_with_arduino, esp32_rmt.validate_rmt_channel(tx=False) - ), cv.SplitDefault( CONF_RMT_SYMBOLS, - esp32_idf=192, - esp32_s2_idf=192, - esp32_s3_idf=192, - esp32_p4_idf=192, - esp32_c3_idf=96, - esp32_c5_idf=96, - esp32_c6_idf=96, - esp32_h2_idf=96, - ): cv.All(cv.only_with_esp_idf, cv.int_range(min=2)), + esp32=192, + esp32_s2=192, + esp32_s3=192, + esp32_p4=192, + esp32_c3=96, + esp32_c5=96, + esp32_c6=96, + esp32_h2=96, + ): cv.All(cv.only_on_esp32, cv.int_range(min=2)), cv.Optional(CONF_FILTER_SYMBOLS): cv.All( - cv.only_with_esp_idf, cv.int_range(min=0) + cv.only_on_esp32, cv.int_range(min=0) ), cv.SplitDefault( CONF_RECEIVE_SYMBOLS, - esp32_idf=192, - ): cv.All(cv.only_with_esp_idf, cv.int_range(min=2)), + esp32=192, + ): cv.All(cv.only_on_esp32, cv.int_range(min=2)), cv.Optional(CONF_USE_DMA): cv.All( esp32.only_on_variant( supported=[esp32.const.VARIANT_ESP32S3, esp32.const.VARIANT_ESP32P4] ), - cv.only_with_esp_idf, cv.boolean, ), } @@ -156,24 +143,15 @@ CONFIG_SCHEMA = remote_base.validate_triggers( async def to_code(config): pin = await cg.gpio_pin_expression(config[CONF_PIN]) if CORE.is_esp32: - if esp32_rmt.use_new_rmt_driver(): - var = cg.new_Pvariable(config[CONF_ID], pin) - cg.add(var.set_rmt_symbols(config[CONF_RMT_SYMBOLS])) - cg.add(var.set_receive_symbols(config[CONF_RECEIVE_SYMBOLS])) - if CONF_USE_DMA in config: - cg.add(var.set_with_dma(config[CONF_USE_DMA])) - if CONF_CLOCK_RESOLUTION in config: - cg.add(var.set_clock_resolution(config[CONF_CLOCK_RESOLUTION])) - if CONF_FILTER_SYMBOLS in config: - cg.add(var.set_filter_symbols(config[CONF_FILTER_SYMBOLS])) - else: - if (rmt_channel := config.get(CONF_RMT_CHANNEL, None)) is not None: - var = cg.new_Pvariable( - config[CONF_ID], pin, rmt_channel, config[CONF_MEMORY_BLOCKS] - ) - else: - var = cg.new_Pvariable(config[CONF_ID], pin, config[CONF_MEMORY_BLOCKS]) - cg.add(var.set_clock_divider(config[CONF_CLOCK_DIVIDER])) + var = cg.new_Pvariable(config[CONF_ID], pin) + cg.add(var.set_rmt_symbols(config[CONF_RMT_SYMBOLS])) + cg.add(var.set_receive_symbols(config[CONF_RECEIVE_SYMBOLS])) + if CONF_USE_DMA in config: + cg.add(var.set_with_dma(config[CONF_USE_DMA])) + if CONF_CLOCK_RESOLUTION in config: + cg.add(var.set_clock_resolution(config[CONF_CLOCK_RESOLUTION])) + if CONF_FILTER_SYMBOLS in config: + cg.add(var.set_filter_symbols(config[CONF_FILTER_SYMBOLS])) else: var = cg.new_Pvariable(config[CONF_ID], pin) @@ -194,3 +172,19 @@ async def to_code(config): cg.add(var.set_buffer_size(config[CONF_BUFFER_SIZE])) cg.add(var.set_filter_us(config[CONF_FILTER])) cg.add(var.set_idle_us(config[CONF_IDLE])) + + +FILTER_SOURCE_FILES = filter_source_files_from_platform( + { + "remote_receiver_esp32.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + }, + "remote_receiver_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, + "remote_receiver_libretiny.cpp": { + PlatformFramework.BK72XX_ARDUINO, + PlatformFramework.RTL87XX_ARDUINO, + PlatformFramework.LN882X_ARDUINO, + }, + } +) diff --git a/esphome/components/remote_receiver/remote_receiver.h b/esphome/components/remote_receiver/remote_receiver.h index 8d19d5490f..45e06e664a 100644 --- a/esphome/components/remote_receiver/remote_receiver.h +++ b/esphome/components/remote_receiver/remote_receiver.h @@ -5,7 +5,7 @@ #include -#if defined(USE_ESP32) && ESP_IDF_VERSION_MAJOR >= 5 +#if defined(USE_ESP32) #include #endif @@ -29,7 +29,7 @@ struct RemoteReceiverComponentStore { uint32_t filter_us{10}; ISRInternalGPIOPin pin; }; -#elif defined(USE_ESP32) && ESP_IDF_VERSION_MAJOR >= 5 +#elif defined(USE_ESP32) struct RemoteReceiverComponentStore { /// Stores RMT symbols and rx done event data volatile uint8_t *buffer{nullptr}; @@ -55,21 +55,12 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, { public: -#if defined(USE_ESP32) && ESP_IDF_VERSION_MAJOR < 5 - RemoteReceiverComponent(InternalGPIOPin *pin, uint8_t mem_block_num = 1) - : RemoteReceiverBase(pin), remote_base::RemoteRMTChannel(mem_block_num) {} - - RemoteReceiverComponent(InternalGPIOPin *pin, rmt_channel_t channel, uint8_t mem_block_num = 1) - : RemoteReceiverBase(pin), remote_base::RemoteRMTChannel(channel, mem_block_num) {} -#else RemoteReceiverComponent(InternalGPIOPin *pin) : RemoteReceiverBase(pin) {} -#endif void setup() override; void dump_config() override; void loop() override; - float get_setup_priority() const override { return setup_priority::DATA; } -#if defined(USE_ESP32) && ESP_IDF_VERSION_MAJOR >= 5 +#ifdef USE_ESP32 void set_filter_symbols(uint32_t filter_symbols) { this->filter_symbols_ = filter_symbols; } void set_receive_symbols(uint32_t receive_symbols) { this->receive_symbols_ = receive_symbols; } void set_with_dma(bool with_dma) { this->with_dma_ = with_dma; } @@ -80,21 +71,16 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, protected: #ifdef USE_ESP32 -#if ESP_IDF_VERSION_MAJOR >= 5 void decode_rmt_(rmt_symbol_word_t *item, size_t item_count); rmt_channel_handle_t channel_{NULL}; uint32_t filter_symbols_{0}; uint32_t receive_symbols_{0}; bool with_dma_{false}; -#else - void decode_rmt_(rmt_item32_t *item, size_t item_count); - RingbufHandle_t ringbuf_; -#endif esp_err_t error_code_{ESP_OK}; std::string error_string_{""}; #endif -#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || (defined(USE_ESP32) && ESP_IDF_VERSION_MAJOR >= 5) +#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_ESP32) RemoteReceiverComponentStore store_; HighFrequencyLoopRequester high_freq_; #endif diff --git a/esphome/components/remote_receiver/remote_receiver_esp32.cpp b/esphome/components/remote_receiver/remote_receiver_esp32.cpp index b78928d857..3d6346baec 100644 --- a/esphome/components/remote_receiver/remote_receiver_esp32.cpp +++ b/esphome/components/remote_receiver/remote_receiver_esp32.cpp @@ -14,7 +14,6 @@ static const uint32_t RMT_CLK_FREQ = 32000000; static const uint32_t RMT_CLK_FREQ = 80000000; #endif -#if ESP_IDF_VERSION_MAJOR >= 5 static bool IRAM_ATTR HOT rmt_callback(rmt_channel_handle_t channel, const rmt_rx_done_event_data_t *event, void *arg) { RemoteReceiverComponentStore *store = (RemoteReceiverComponentStore *) arg; rmt_rx_done_event_data_t *event_buffer = (rmt_rx_done_event_data_t *) (store->buffer + store->buffer_write); @@ -37,11 +36,9 @@ static bool IRAM_ATTR HOT rmt_callback(rmt_channel_handle_t channel, const rmt_r store->buffer_write = next_write; return false; } -#endif void RemoteReceiverComponent::setup() { ESP_LOGCONFIG(TAG, "Running setup"); -#if ESP_IDF_VERSION_MAJOR >= 5 rmt_rx_channel_config_t channel; memset(&channel, 0, sizeof(channel)); channel.clk_src = RMT_CLK_SRC_DEFAULT; @@ -105,62 +102,11 @@ void RemoteReceiverComponent::setup() { this->mark_failed(); return; } -#else - this->pin_->setup(); - rmt_config_t rmt{}; - this->config_rmt(rmt); - rmt.gpio_num = gpio_num_t(this->pin_->get_pin()); - rmt.rmt_mode = RMT_MODE_RX; - if (this->filter_us_ == 0) { - rmt.rx_config.filter_en = false; - } else { - rmt.rx_config.filter_en = true; - rmt.rx_config.filter_ticks_thresh = static_cast( - std::min(this->from_microseconds_(this->filter_us_) * this->clock_divider_, (uint32_t) 255)); - } - rmt.rx_config.idle_threshold = - static_cast(std::min(this->from_microseconds_(this->idle_us_), (uint32_t) 65535)); - - esp_err_t error = rmt_config(&rmt); - if (error != ESP_OK) { - this->error_code_ = error; - this->error_string_ = "in rmt_config"; - this->mark_failed(); - return; - } - - error = rmt_driver_install(this->channel_, this->buffer_size_, 0); - if (error != ESP_OK) { - this->error_code_ = error; - if (error == ESP_ERR_INVALID_STATE) { - this->error_string_ = str_sprintf("RMT channel %i is already in use by another component", this->channel_); - } else { - this->error_string_ = "in rmt_driver_install"; - } - this->mark_failed(); - return; - } - error = rmt_get_ringbuf_handle(this->channel_, &this->ringbuf_); - if (error != ESP_OK) { - this->error_code_ = error; - this->error_string_ = "in rmt_get_ringbuf_handle"; - this->mark_failed(); - return; - } - error = rmt_rx_start(this->channel_, true); - if (error != ESP_OK) { - this->error_code_ = error; - this->error_string_ = "in rmt_rx_start"; - this->mark_failed(); - return; - } -#endif } void RemoteReceiverComponent::dump_config() { ESP_LOGCONFIG(TAG, "Remote Receiver:"); LOG_PIN(" Pin: ", this->pin_); -#if ESP_IDF_VERSION_MAJOR >= 5 ESP_LOGCONFIG(TAG, " Clock resolution: %" PRIu32 " hz\n" " RMT symbols: %" PRIu32 "\n" @@ -172,22 +118,6 @@ void RemoteReceiverComponent::dump_config() { this->clock_resolution_, this->rmt_symbols_, this->filter_symbols_, this->receive_symbols_, this->tolerance_, (this->tolerance_mode_ == remote_base::TOLERANCE_MODE_TIME) ? " us" : "%", this->filter_us_, this->idle_us_); -#else - if (this->pin_->digital_read()) { - ESP_LOGW(TAG, "Remote Receiver Signal starts with a HIGH value. Usually this means you have to " - "invert the signal using 'inverted: True' in the pin schema!"); - } - ESP_LOGCONFIG(TAG, - " Channel: %d\n" - " RMT memory blocks: %d\n" - " Clock divider: %u\n" - " Tolerance: %" PRIu32 "%s\n" - " Filter out pulses shorter than: %" PRIu32 " us\n" - " Signal is done after %" PRIu32 " us of no changes", - this->channel_, this->mem_block_num_, this->clock_divider_, this->tolerance_, - (this->tolerance_mode_ == remote_base::TOLERANCE_MODE_TIME) ? " us" : "%", this->filter_us_, - this->idle_us_); -#endif if (this->is_failed()) { ESP_LOGE(TAG, "Configuring RMT driver failed: %s (%s)", esp_err_to_name(this->error_code_), this->error_string_.c_str()); @@ -195,7 +125,6 @@ void RemoteReceiverComponent::dump_config() { } void RemoteReceiverComponent::loop() { -#if ESP_IDF_VERSION_MAJOR >= 5 if (this->store_.error != ESP_OK) { ESP_LOGE(TAG, "Receive error"); this->error_code_ = this->store_.error; @@ -221,25 +150,9 @@ void RemoteReceiverComponent::loop() { this->call_listeners_dumpers_(); } } -#else - size_t len = 0; - auto *item = (rmt_item32_t *) xRingbufferReceive(this->ringbuf_, &len, 0); - if (item != nullptr) { - this->decode_rmt_(item, len / sizeof(rmt_item32_t)); - vRingbufferReturnItem(this->ringbuf_, item); - - if (!this->temp_.empty()) { - this->call_listeners_dumpers_(); - } - } -#endif } -#if ESP_IDF_VERSION_MAJOR >= 5 void RemoteReceiverComponent::decode_rmt_(rmt_symbol_word_t *item, size_t item_count) { -#else -void RemoteReceiverComponent::decode_rmt_(rmt_item32_t *item, size_t item_count) { -#endif bool prev_level = false; bool idle_level = false; uint32_t prev_length = 0; diff --git a/esphome/components/remote_receiver/remote_receiver_esp8266.cpp b/esphome/components/remote_receiver/remote_receiver_esp8266.cpp index a0fd56bcf4..fe935ba227 100644 --- a/esphome/components/remote_receiver/remote_receiver_esp8266.cpp +++ b/esphome/components/remote_receiver/remote_receiver_esp8266.cpp @@ -27,7 +27,7 @@ void IRAM_ATTR HOT RemoteReceiverComponentStore::gpio_intr(RemoteReceiverCompone if (time_since_change <= arg->filter_us) return; - arg->buffer[arg->buffer_write_at = next] = now; + arg->buffer[arg->buffer_write_at = next] = now; // NOLINT(clang-diagnostic-deprecated-volatile) } void RemoteReceiverComponent::setup() { diff --git a/esphome/components/remote_transmitter/__init__.py b/esphome/components/remote_transmitter/__init__.py index 4db24760d8..47a46ff56b 100644 --- a/esphome/components/remote_transmitter/__init__.py +++ b/esphome/components/remote_transmitter/__init__.py @@ -1,19 +1,19 @@ from esphome import automation, pins import esphome.codegen as cg from esphome.components import esp32, esp32_rmt, remote_base +from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( CONF_CARRIER_DUTY_PERCENT, - CONF_CLOCK_DIVIDER, CONF_CLOCK_RESOLUTION, CONF_ID, CONF_INVERTED, CONF_MODE, CONF_OPEN_DRAIN, CONF_PIN, - CONF_RMT_CHANNEL, CONF_RMT_SYMBOLS, CONF_USE_DMA, + PlatformFramework, ) from esphome.core import CORE @@ -38,34 +38,26 @@ CONFIG_SCHEMA = cv.Schema( ), cv.Optional(CONF_CLOCK_RESOLUTION): cv.All( cv.only_on_esp32, - cv.only_with_esp_idf, esp32_rmt.validate_clock_resolution(), ), - cv.Optional(CONF_CLOCK_DIVIDER): cv.All( - cv.only_on_esp32, cv.only_with_arduino, cv.int_range(min=1, max=255) - ), - cv.Optional(CONF_EOT_LEVEL): cv.All(cv.only_with_esp_idf, cv.boolean), + cv.Optional(CONF_EOT_LEVEL): cv.All(cv.only_on_esp32, cv.boolean), cv.Optional(CONF_USE_DMA): cv.All( esp32.only_on_variant( supported=[esp32.const.VARIANT_ESP32S3, esp32.const.VARIANT_ESP32P4] ), - cv.only_with_esp_idf, cv.boolean, ), cv.SplitDefault( CONF_RMT_SYMBOLS, - esp32_idf=64, - esp32_s2_idf=64, - esp32_s3_idf=48, - esp32_p4_idf=48, - esp32_c3_idf=48, - esp32_c5_idf=48, - esp32_c6_idf=48, - esp32_h2_idf=48, - ): cv.All(cv.only_with_esp_idf, cv.int_range(min=2)), - cv.Optional(CONF_RMT_CHANNEL): cv.All( - cv.only_with_arduino, esp32_rmt.validate_rmt_channel(tx=True) - ), + esp32=64, + esp32_s2=64, + esp32_s3=48, + esp32_p4=48, + esp32_c3=48, + esp32_c5=48, + esp32_c6=48, + esp32_h2=48, + ): cv.All(cv.only_on_esp32, cv.int_range(min=2)), cv.Optional(CONF_ON_TRANSMIT): automation.validate_automation(single=True), cv.Optional(CONF_ON_COMPLETE): automation.validate_automation(single=True), } @@ -75,30 +67,21 @@ CONFIG_SCHEMA = cv.Schema( async def to_code(config): pin = await cg.gpio_pin_expression(config[CONF_PIN]) if CORE.is_esp32: - if esp32_rmt.use_new_rmt_driver(): - var = cg.new_Pvariable(config[CONF_ID], pin) - cg.add(var.set_rmt_symbols(config[CONF_RMT_SYMBOLS])) - if CONF_CLOCK_RESOLUTION in config: - cg.add(var.set_clock_resolution(config[CONF_CLOCK_RESOLUTION])) - if CONF_USE_DMA in config: - cg.add(var.set_with_dma(config[CONF_USE_DMA])) - if CONF_EOT_LEVEL in config: - cg.add(var.set_eot_level(config[CONF_EOT_LEVEL])) - else: - cg.add( - var.set_eot_level( - config[CONF_PIN][CONF_MODE][CONF_OPEN_DRAIN] - or config[CONF_PIN][CONF_INVERTED] - ) - ) + var = cg.new_Pvariable(config[CONF_ID], pin) + cg.add(var.set_rmt_symbols(config[CONF_RMT_SYMBOLS])) + if CONF_CLOCK_RESOLUTION in config: + cg.add(var.set_clock_resolution(config[CONF_CLOCK_RESOLUTION])) + if CONF_USE_DMA in config: + cg.add(var.set_with_dma(config[CONF_USE_DMA])) + if CONF_EOT_LEVEL in config: + cg.add(var.set_eot_level(config[CONF_EOT_LEVEL])) else: - if (rmt_channel := config.get(CONF_RMT_CHANNEL, None)) is not None: - var = cg.new_Pvariable(config[CONF_ID], pin, rmt_channel) - else: - var = cg.new_Pvariable(config[CONF_ID], pin) - if CONF_CLOCK_DIVIDER in config: - cg.add(var.set_clock_divider(config[CONF_CLOCK_DIVIDER])) - + cg.add( + var.set_eot_level( + config[CONF_PIN][CONF_MODE][CONF_OPEN_DRAIN] + or config[CONF_PIN][CONF_INVERTED] + ) + ) else: var = cg.new_Pvariable(config[CONF_ID], pin) await cg.register_component(var, config) @@ -114,3 +97,19 @@ async def to_code(config): await automation.build_automation( var.get_complete_trigger(), [], on_complete_config ) + + +FILTER_SOURCE_FILES = filter_source_files_from_platform( + { + "remote_transmitter_esp32.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + }, + "remote_transmitter_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, + "remote_transmitter_libretiny.cpp": { + PlatformFramework.BK72XX_ARDUINO, + PlatformFramework.RTL87XX_ARDUINO, + PlatformFramework.LN882X_ARDUINO, + }, + } +) diff --git a/esphome/components/remote_transmitter/remote_transmitter.h b/esphome/components/remote_transmitter/remote_transmitter.h index 0a8f354c72..f0dab2aaf8 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.h +++ b/esphome/components/remote_transmitter/remote_transmitter.h @@ -5,7 +5,7 @@ #include -#if defined(USE_ESP32) && ESP_IDF_VERSION_MAJOR >= 5 +#if defined(USE_ESP32) #include #endif @@ -20,15 +20,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, #endif { public: -#if defined(USE_ESP32) && ESP_IDF_VERSION_MAJOR < 5 - RemoteTransmitterComponent(InternalGPIOPin *pin, uint8_t mem_block_num = 1) - : remote_base::RemoteTransmitterBase(pin), remote_base::RemoteRMTChannel(mem_block_num) {} - - RemoteTransmitterComponent(InternalGPIOPin *pin, rmt_channel_t channel, uint8_t mem_block_num = 1) - : remote_base::RemoteTransmitterBase(pin), remote_base::RemoteRMTChannel(channel, mem_block_num) {} -#else explicit RemoteTransmitterComponent(InternalGPIOPin *pin) : remote_base::RemoteTransmitterBase(pin) {} -#endif void setup() override; void dump_config() override; @@ -38,7 +30,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, void set_carrier_duty_percent(uint8_t carrier_duty_percent) { this->carrier_duty_percent_ = carrier_duty_percent; } -#if defined(USE_ESP32) && ESP_IDF_VERSION_MAJOR >= 5 +#if defined(USE_ESP32) void set_with_dma(bool with_dma) { this->with_dma_ = with_dma; } void set_eot_level(bool eot_level) { this->eot_level_ = eot_level; } void digital_write(bool value); @@ -65,15 +57,11 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, uint32_t current_carrier_frequency_{38000}; bool initialized_{false}; -#if ESP_IDF_VERSION_MAJOR >= 5 std::vector rmt_temp_; bool with_dma_{false}; bool eot_level_{false}; rmt_channel_handle_t channel_{NULL}; rmt_encoder_handle_t encoder_{NULL}; -#else - std::vector rmt_temp_; -#endif esp_err_t error_code_{ESP_OK}; std::string error_string_{""}; bool inverted_{false}; diff --git a/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp b/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp index d51c45c607..411e380670 100644 --- a/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp @@ -18,18 +18,10 @@ void RemoteTransmitterComponent::setup() { void RemoteTransmitterComponent::dump_config() { ESP_LOGCONFIG(TAG, "Remote Transmitter:"); -#if ESP_IDF_VERSION_MAJOR >= 5 ESP_LOGCONFIG(TAG, " Clock resolution: %" PRIu32 " hz\n" " RMT symbols: %" PRIu32, this->clock_resolution_, this->rmt_symbols_); -#else - ESP_LOGCONFIG(TAG, - " Channel: %d\n" - " RMT memory blocks: %d\n" - " Clock divider: %u", - this->channel_, this->mem_block_num_, this->clock_divider_); -#endif LOG_PIN(" Pin: ", this->pin_); if (this->current_carrier_frequency_ != 0 && this->carrier_duty_percent_ != 100) { @@ -42,7 +34,6 @@ void RemoteTransmitterComponent::dump_config() { } } -#if ESP_IDF_VERSION_MAJOR >= 5 void RemoteTransmitterComponent::digital_write(bool value) { rmt_symbol_word_t symbol = { .duration0 = 1, @@ -65,10 +56,8 @@ void RemoteTransmitterComponent::digital_write(bool value) { this->status_set_warning(); } } -#endif void RemoteTransmitterComponent::configure_rmt_() { -#if ESP_IDF_VERSION_MAJOR >= 5 esp_err_t error; if (!this->initialized_) { @@ -140,54 +129,6 @@ void RemoteTransmitterComponent::configure_rmt_() { this->mark_failed(); return; } -#else - rmt_config_t c{}; - - this->config_rmt(c); - c.rmt_mode = RMT_MODE_TX; - c.gpio_num = gpio_num_t(this->pin_->get_pin()); - c.tx_config.loop_en = false; - - if (this->current_carrier_frequency_ == 0 || this->carrier_duty_percent_ == 100) { - c.tx_config.carrier_en = false; - } else { - c.tx_config.carrier_en = true; - c.tx_config.carrier_freq_hz = this->current_carrier_frequency_; - c.tx_config.carrier_duty_percent = this->carrier_duty_percent_; - } - - c.tx_config.idle_output_en = true; - if (!this->inverted_) { - c.tx_config.carrier_level = RMT_CARRIER_LEVEL_HIGH; - c.tx_config.idle_level = RMT_IDLE_LEVEL_LOW; - } else { - c.tx_config.carrier_level = RMT_CARRIER_LEVEL_LOW; - c.tx_config.idle_level = RMT_IDLE_LEVEL_HIGH; - } - - esp_err_t error = rmt_config(&c); - if (error != ESP_OK) { - this->error_code_ = error; - this->error_string_ = "in rmt_config"; - this->mark_failed(); - return; - } - - if (!this->initialized_) { - error = rmt_driver_install(this->channel_, 0, 0); - if (error != ESP_OK) { - this->error_code_ = error; - if (error == ESP_ERR_INVALID_STATE) { - this->error_string_ = str_sprintf("RMT channel %i is already in use by another component", this->channel_); - } else { - this->error_string_ = "in rmt_driver_install"; - } - this->mark_failed(); - return; - } - this->initialized_ = true; - } -#endif } void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) { @@ -202,11 +143,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen this->rmt_temp_.clear(); this->rmt_temp_.reserve((this->temp_.get_data().size() + 1) / 2); uint32_t rmt_i = 0; -#if ESP_IDF_VERSION_MAJOR >= 5 rmt_symbol_word_t rmt_item; -#else - rmt_item32_t rmt_item; -#endif for (int32_t val : this->temp_.get_data()) { bool level = val >= 0; @@ -241,7 +178,6 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen return; } this->transmit_trigger_->trigger(); -#if ESP_IDF_VERSION_MAJOR >= 5 for (uint32_t i = 0; i < send_times; i++) { rmt_transmit_config_t config; memset(&config, 0, sizeof(config)); @@ -263,19 +199,6 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen if (i + 1 < send_times) delayMicroseconds(send_wait); } -#else - for (uint32_t i = 0; i < send_times; i++) { - esp_err_t error = rmt_write_items(this->channel_, this->rmt_temp_.data(), this->rmt_temp_.size(), true); - if (error != ESP_OK) { - ESP_LOGW(TAG, "rmt_write_items failed: %s", esp_err_to_name(error)); - this->status_set_warning(); - } else { - this->status_clear_warning(); - } - if (i + 1 < send_times) - delayMicroseconds(send_wait); - } -#endif this->complete_trigger_->trigger(); } diff --git a/esphome/components/resistance/resistance_sensor.h b/esphome/components/resistance/resistance_sensor.h index b57f90b59c..a3b6e92c59 100644 --- a/esphome/components/resistance/resistance_sensor.h +++ b/esphome/components/resistance/resistance_sensor.h @@ -24,7 +24,6 @@ class ResistanceSensor : public Component, public sensor::Sensor { this->process_(this->sensor_->state); } void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: void process_(float value); diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index c3e11336a9..ecbeb83bb4 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -167,6 +167,7 @@ async def to_code(config): cg.add_platformio_option("lib_ldf_mode", "chain+") cg.add_platformio_option("board", config[CONF_BOARD]) cg.add_build_flag("-DUSE_RP2040") + cg.set_cpp_standard("gnu++20") cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_define("ESPHOME_VARIANT", "RP2040") diff --git a/esphome/components/rp2040/helpers.cpp b/esphome/components/rp2040/helpers.cpp new file mode 100644 index 0000000000..a6eac58dc6 --- /dev/null +++ b/esphome/components/rp2040/helpers.cpp @@ -0,0 +1,55 @@ +#include "esphome/core/helpers.h" +#include "esphome/core/defines.h" + +#ifdef USE_RP2040 + +#include "esphome/core/hal.h" + +#if defined(USE_WIFI) +#include +#endif +#include +#include + +namespace esphome { + +uint32_t random_uint32() { + uint32_t result = 0; + for (uint8_t i = 0; i < 32; i++) { + result <<= 1; + result |= rosc_hw->randombit; + } + return result; +} + +bool random_bytes(uint8_t *data, size_t len) { + while (len-- != 0) { + uint8_t result = 0; + for (uint8_t i = 0; i < 8; i++) { + result <<= 1; + result |= rosc_hw->randombit; + } + *data++ = result; + } + return true; +} + +// RP2040 doesn't have mutexes, but that shouldn't be an issue as it's single-core and non-preemptive OS. +Mutex::Mutex() {} +Mutex::~Mutex() {} +void Mutex::lock() {} +bool Mutex::try_lock() { return true; } +void Mutex::unlock() {} + +IRAM_ATTR InterruptLock::InterruptLock() { state_ = save_and_disable_interrupts(); } +IRAM_ATTR InterruptLock::~InterruptLock() { restore_interrupts(state_); } + +void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter) +#ifdef USE_WIFI + WiFi.macAddress(mac); +#endif +} + +} // namespace esphome + +#endif // USE_RP2040 diff --git a/esphome/components/rp2040_pio_led_strip/led_strip.cpp b/esphome/components/rp2040_pio_led_strip/led_strip.cpp index a6ff037d88..42f7e9cf52 100644 --- a/esphome/components/rp2040_pio_led_strip/led_strip.cpp +++ b/esphome/components/rp2040_pio_led_strip/led_strip.cpp @@ -9,8 +9,8 @@ #include #include #include -#include #include +#include namespace esphome { namespace rp2040_pio_led_strip { @@ -44,7 +44,7 @@ void RP2040PIOLEDStripLightOutput::setup() { size_t buffer_size = this->get_buffer_size_(); - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator allocator; this->buf_ = allocator.allocate(buffer_size); if (this->buf_ == nullptr) { ESP_LOGE(TAG, "Failed to allocate buffer of size %u", buffer_size); diff --git a/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp b/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp index 666bac354d..91eb947a3e 100644 --- a/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp +++ b/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp @@ -10,10 +10,8 @@ void RpiDpiRgb::setup() { this->reset_display_(); esp_lcd_rgb_panel_config_t config{}; config.flags.fb_in_psram = 1; -#if ESP_IDF_VERSION_MAJOR >= 5 config.bounce_buffer_size_px = this->width_ * 10; config.num_fbs = 1; -#endif // ESP_IDF_VERSION_MAJOR config.timings.h_res = this->width_; config.timings.v_res = this->height_; config.timings.hsync_pulse_width = this->hsync_pulse_width_; @@ -47,10 +45,8 @@ void RpiDpiRgb::setup() { ESP_LOGCONFIG(TAG, "RPI_DPI_RGB setup complete"); } void RpiDpiRgb::loop() { -#if ESP_IDF_VERSION_MAJOR >= 5 if (this->handle_ != nullptr) esp_lcd_rgb_panel_restart(this->handle_); -#endif // ESP_IDF_VERSION_MAJOR } void RpiDpiRgb::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order, diff --git a/esphome/components/rtttl/__init__.py b/esphome/components/rtttl/__init__.py index 0abd51a6f1..ebbe5366aa 100644 --- a/esphome/components/rtttl/__init__.py +++ b/esphome/components/rtttl/__init__.py @@ -57,14 +57,14 @@ def validate_parent_output_config(value): platform = value.get(CONF_PLATFORM) PWM_GOOD = ["esp8266_pwm", "ledc"] PWM_BAD = [ - "ac_dimmer ", + "ac_dimmer", "esp32_dac", - "slow_pwm", "mcp4725", - "pca9685", - "tlc59208f", "my9231", + "pca9685", + "slow_pwm", "sm16716", + "tlc59208f", ] if platform in PWM_BAD: diff --git a/esphome/components/rtttl/rtttl.cpp b/esphome/components/rtttl/rtttl.cpp index e24816fd83..65a3af1bbc 100644 --- a/esphome/components/rtttl/rtttl.cpp +++ b/esphome/components/rtttl/rtttl.cpp @@ -142,8 +142,10 @@ void Rtttl::stop() { } void Rtttl::loop() { - if (this->note_duration_ == 0 || this->state_ == State::STATE_STOPPED) + if (this->note_duration_ == 0 || this->state_ == State::STATE_STOPPED) { + this->disable_loop(); return; + } #ifdef USE_SPEAKER if (this->speaker_ != nullptr) { @@ -369,6 +371,7 @@ void Rtttl::finish_() { ESP_LOGD(TAG, "Playback finished"); } +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG static const LogString *state_to_string(State state) { switch (state) { case STATE_STOPPED: @@ -385,12 +388,18 @@ static const LogString *state_to_string(State state) { return LOG_STR("UNKNOWN"); } }; +#endif void Rtttl::set_state_(State state) { State old_state = this->state_; this->state_ = state; ESP_LOGV(TAG, "State changed from %s to %s", LOG_STR_ARG(state_to_string(old_state)), LOG_STR_ARG(state_to_string(state))); + + // Clear loop_done when transitioning from STOPPED to any other state + if (old_state == State::STATE_STOPPED && state != State::STATE_STOPPED) { + this->enable_loop(); + } } } // namespace rtttl diff --git a/esphome/components/ruuvitag/ruuvitag.h b/esphome/components/ruuvitag/ruuvitag.h index 63029ebb4d..dfe393724c 100644 --- a/esphome/components/ruuvitag/ruuvitag.h +++ b/esphome/components/ruuvitag/ruuvitag.h @@ -48,7 +48,6 @@ class RuuviTag : public Component, public esp32_ble_tracker::ESPBTDeviceListener } void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } void set_pressure(sensor::Sensor *pressure) { pressure_ = pressure; } diff --git a/esphome/components/safe_mode/safe_mode.cpp b/esphome/components/safe_mode/safe_mode.cpp index 89c9242357..5a62604269 100644 --- a/esphome/components/safe_mode/safe_mode.cpp +++ b/esphome/components/safe_mode/safe_mode.cpp @@ -42,6 +42,8 @@ void SafeModeComponent::loop() { ESP_LOGI(TAG, "Boot seems successful; resetting boot loop counter"); this->clean_rtc(); this->boot_successful_ = true; + // Disable loop since we no longer need to check + this->disable_loop(); } } diff --git a/esphome/components/safe_mode/safe_mode.h b/esphome/components/safe_mode/safe_mode.h index 37e2c3a3d6..028b7b11cb 100644 --- a/esphome/components/safe_mode/safe_mode.h +++ b/esphome/components/safe_mode/safe_mode.h @@ -33,12 +33,15 @@ class SafeModeComponent : public Component { void write_rtc_(uint32_t val); uint32_t read_rtc_(); - bool boot_successful_{false}; ///< set to true after boot is considered successful + // Group all 4-byte aligned members together to avoid padding uint32_t safe_mode_boot_is_good_after_{60000}; ///< The amount of time after which the boot is considered successful uint32_t safe_mode_enable_time_{60000}; ///< The time safe mode should remain active for uint32_t safe_mode_rtc_value_{0}; uint32_t safe_mode_start_time_{0}; ///< stores when safe mode was enabled + // Group 1-byte members together to minimize padding + bool boot_successful_{false}; ///< set to true after boot is considered successful uint8_t safe_mode_num_attempts_{0}; + // Larger objects at the end ESPPreferenceObject rtc_; CallbackManager safe_mode_callback_{}; diff --git a/esphome/components/scd30/scd30.h b/esphome/components/scd30/scd30.h index 40f075e673..ed3f5e7e9a 100644 --- a/esphome/components/scd30/scd30.h +++ b/esphome/components/scd30/scd30.h @@ -26,7 +26,6 @@ class SCD30Component : public Component, public sensirion_common::SensirionI2CDe void setup() override; void update(); void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: bool is_data_ready_(); diff --git a/esphome/components/scd30/sensor.py b/esphome/components/scd30/sensor.py index fb3ad713bb..f341d2a47b 100644 --- a/esphome/components/scd30/sensor.py +++ b/esphome/components/scd30/sensor.py @@ -3,6 +3,8 @@ import esphome.codegen as cg from esphome.components import i2c, sensirion_common, sensor import esphome.config_validation as cv from esphome.const import ( + CONF_AMBIENT_PRESSURE_COMPENSATION, + CONF_AUTOMATIC_SELF_CALIBRATION, CONF_CO2, CONF_HUMIDITY, CONF_ID, @@ -18,8 +20,6 @@ from esphome.const import ( UNIT_CELSIUS, UNIT_PARTS_PER_MILLION, UNIT_PERCENT, - CONF_AUTOMATIC_SELF_CALIBRATION, - CONF_AMBIENT_PRESSURE_COMPENSATION, ) DEPENDENCIES = ["i2c"] diff --git a/esphome/components/scd4x/scd4x.cpp b/esphome/components/scd4x/scd4x.cpp index f617ffe276..06db70e3f3 100644 --- a/esphome/components/scd4x/scd4x.cpp +++ b/esphome/components/scd4x/scd4x.cpp @@ -7,6 +7,8 @@ namespace scd4x { static const char *const TAG = "scd4x"; +static const uint16_t SCD41_ID = 0x1408; +static const uint16_t SCD40_ID = 0x440; static const uint16_t SCD4X_CMD_GET_SERIAL_NUMBER = 0x3682; static const uint16_t SCD4X_CMD_TEMPERATURE_OFFSET = 0x241d; static const uint16_t SCD4X_CMD_ALTITUDE_COMPENSATION = 0x2427; @@ -23,8 +25,6 @@ static const uint16_t SCD4X_CMD_STOP_MEASUREMENTS = 0x3f86; static const uint16_t SCD4X_CMD_FACTORY_RESET = 0x3632; static const uint16_t SCD4X_CMD_GET_FEATURESET = 0x202f; static const float SCD4X_TEMPERATURE_OFFSET_MULTIPLIER = (1 << 16) / 175.0f; -static const uint16_t SCD41_ID = 0x1408; -static const uint16_t SCD40_ID = 0x440; void SCD4XComponent::setup() { ESP_LOGCONFIG(TAG, "Running setup"); @@ -51,47 +51,66 @@ void SCD4XComponent::setup() { if (!this->write_command(SCD4X_CMD_TEMPERATURE_OFFSET, (uint16_t) (temperature_offset_ * SCD4X_TEMPERATURE_OFFSET_MULTIPLIER))) { - ESP_LOGE(TAG, "Error setting temperature offset."); + ESP_LOGE(TAG, "Error setting temperature offset"); this->error_code_ = MEASUREMENT_INIT_FAILED; this->mark_failed(); return; } - // If pressure compensation available use it - // else use altitude - if (ambient_pressure_compensation_) { - if (!this->update_ambient_pressure_compensation_(ambient_pressure_)) { - ESP_LOGE(TAG, "Error setting ambient pressure compensation."); + // If pressure compensation available use it, else use altitude + if (this->ambient_pressure_) { + if (!this->update_ambient_pressure_compensation_(this->ambient_pressure_)) { + ESP_LOGE(TAG, "Error setting ambient pressure compensation"); this->error_code_ = MEASUREMENT_INIT_FAILED; this->mark_failed(); return; } } else { - if (!this->write_command(SCD4X_CMD_ALTITUDE_COMPENSATION, altitude_compensation_)) { - ESP_LOGE(TAG, "Error setting altitude compensation."); + if (!this->write_command(SCD4X_CMD_ALTITUDE_COMPENSATION, this->altitude_compensation_)) { + ESP_LOGE(TAG, "Error setting altitude compensation"); this->error_code_ = MEASUREMENT_INIT_FAILED; this->mark_failed(); return; } } - if (!this->write_command(SCD4X_CMD_AUTOMATIC_SELF_CALIBRATION, enable_asc_ ? 1 : 0)) { - ESP_LOGE(TAG, "Error setting automatic self calibration."); + if (!this->write_command(SCD4X_CMD_AUTOMATIC_SELF_CALIBRATION, this->enable_asc_ ? 1 : 0)) { + ESP_LOGE(TAG, "Error setting automatic self calibration"); this->error_code_ = MEASUREMENT_INIT_FAILED; this->mark_failed(); return; } - initialized_ = true; + this->initialized_ = true; // Finally start sensor measurements this->start_measurement_(); - ESP_LOGD(TAG, "Sensor initialized"); }); }); } void SCD4XComponent::dump_config() { - ESP_LOGCONFIG(TAG, "scd4x:"); + static const char *const MM_PERIODIC_STR = "Periodic (5s)"; + static const char *const MM_LOW_POWER_PERIODIC_STR = "Low power periodic (30s)"; + static const char *const MM_SINGLE_SHOT_STR = "Single shot"; + static const char *const MM_SINGLE_SHOT_RHT_ONLY_STR = "Single shot rht only"; + const char *measurement_mode_str = MM_PERIODIC_STR; + + switch (this->measurement_mode_) { + case PERIODIC: + // measurement_mode_str = MM_PERIODIC_STR; + break; + case LOW_POWER_PERIODIC: + measurement_mode_str = MM_LOW_POWER_PERIODIC_STR; + break; + case SINGLE_SHOT: + measurement_mode_str = MM_SINGLE_SHOT_STR; + break; + case SINGLE_SHOT_RHT_ONLY: + measurement_mode_str = MM_SINGLE_SHOT_RHT_ONLY_STR; + break; + } + + ESP_LOGCONFIG(TAG, "SCD4X:"); LOG_I2C_DEVICE(this); if (this->is_failed()) { switch (this->error_code_) { @@ -102,19 +121,23 @@ void SCD4XComponent::dump_config() { ESP_LOGW(TAG, "Measurement Initialization failed"); break; case SERIAL_NUMBER_IDENTIFICATION_FAILED: - ESP_LOGW(TAG, "Unable to read sensor firmware version"); + ESP_LOGW(TAG, "Unable to read firmware version"); break; default: ESP_LOGW(TAG, "Unknown setup error"); break; } } - ESP_LOGCONFIG(TAG, " Automatic self calibration: %s", ONOFF(this->enable_asc_)); + ESP_LOGCONFIG(TAG, + " Automatic self calibration: %s\n" + " Measurement mode: %s\n" + " Temperature offset: %.2f °C", + ONOFF(this->enable_asc_), measurement_mode_str, this->temperature_offset_); if (this->ambient_pressure_source_ != nullptr) { - ESP_LOGCONFIG(TAG, " Dynamic ambient pressure compensation using sensor '%s'", + ESP_LOGCONFIG(TAG, " Dynamic ambient pressure compensation using '%s'", this->ambient_pressure_source_->get_name().c_str()); } else { - if (this->ambient_pressure_compensation_) { + if (this->ambient_pressure_) { ESP_LOGCONFIG(TAG, " Altitude compensation disabled\n" " Ambient pressure compensation: %dmBar", @@ -126,21 +149,6 @@ void SCD4XComponent::dump_config() { this->altitude_compensation_); } } - switch (this->measurement_mode_) { - case PERIODIC: - ESP_LOGCONFIG(TAG, " Measurement mode: periodic (5s)"); - break; - case LOW_POWER_PERIODIC: - ESP_LOGCONFIG(TAG, " Measurement mode: low power periodic (30s)"); - break; - case SINGLE_SHOT: - ESP_LOGCONFIG(TAG, " Measurement mode: single shot"); - break; - case SINGLE_SHOT_RHT_ONLY: - ESP_LOGCONFIG(TAG, " Measurement mode: single shot rht only"); - break; - } - ESP_LOGCONFIG(TAG, " Temperature offset: %.2f °C", this->temperature_offset_); LOG_UPDATE_INTERVAL(this); LOG_SENSOR(" ", "CO2", this->co2_sensor_); LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); @@ -148,20 +156,20 @@ void SCD4XComponent::dump_config() { } void SCD4XComponent::update() { - if (!initialized_) { + if (!this->initialized_) { return; } if (this->ambient_pressure_source_ != nullptr) { float pressure = this->ambient_pressure_source_->state; if (!std::isnan(pressure)) { - set_ambient_pressure_compensation(pressure); + this->set_ambient_pressure_compensation(pressure); } } uint32_t wait_time = 0; if (this->measurement_mode_ == SINGLE_SHOT || this->measurement_mode_ == SINGLE_SHOT_RHT_ONLY) { - start_measurement_(); + this->start_measurement_(); wait_time = this->measurement_mode_ == SINGLE_SHOT ? 5000 : 50; // Single shot measurement takes 5 secs rht mode 50 ms } @@ -176,12 +184,12 @@ void SCD4XComponent::update() { if (!this->read_data(raw_read_status) || raw_read_status == 0x00) { this->status_set_warning(); - ESP_LOGW(TAG, "Data not ready yet!"); + ESP_LOGW(TAG, "Data not ready"); return; } if (!this->write_command(SCD4X_CMD_READ_MEASUREMENT)) { - ESP_LOGW(TAG, "Error reading measurement!"); + ESP_LOGW(TAG, "Error reading measurement"); this->status_set_warning(); return; // NO RETRY } @@ -218,19 +226,19 @@ bool SCD4XComponent::perform_forced_calibration(uint16_t current_co2_concentrati } this->set_timeout(500, [this, current_co2_concentration]() { if (this->write_command(SCD4X_CMD_PERFORM_FORCED_CALIBRATION, current_co2_concentration)) { - ESP_LOGD(TAG, "setting forced calibration Co2 level %d ppm", current_co2_concentration); + ESP_LOGD(TAG, "Setting forced calibration Co2 level %d ppm", current_co2_concentration); // frc takes 400 ms // because this method will be used very rarly // the simple approach with delay is ok - delay(400); // NOLINT' + delay(400); // NOLINT if (!this->start_measurement_()) { return false; } else { - ESP_LOGD(TAG, "forced calibration complete"); + ESP_LOGD(TAG, "Forced calibration complete"); } return true; } else { - ESP_LOGE(TAG, "force calibration failed"); + ESP_LOGE(TAG, "Force calibration failed"); this->error_code_ = FRC_FAILED; this->status_set_warning(); return false; @@ -259,27 +267,26 @@ bool SCD4XComponent::factory_reset() { } void SCD4XComponent::set_ambient_pressure_compensation(float pressure_in_hpa) { - ambient_pressure_compensation_ = true; - uint16_t new_ambient_pressure = (uint16_t) pressure_in_hpa; - if (!initialized_) { - ambient_pressure_ = new_ambient_pressure; + uint16_t new_ambient_pressure = static_cast(pressure_in_hpa); + if (!this->initialized_) { + this->ambient_pressure_ = new_ambient_pressure; return; } // Only send pressure value if it has changed since last update - if (new_ambient_pressure != ambient_pressure_) { - update_ambient_pressure_compensation_(new_ambient_pressure); - ambient_pressure_ = new_ambient_pressure; + if (new_ambient_pressure != this->ambient_pressure_) { + this->update_ambient_pressure_compensation_(new_ambient_pressure); + this->ambient_pressure_ = new_ambient_pressure; } else { - ESP_LOGD(TAG, "ambient pressure compensation skipped - no change required"); + ESP_LOGD(TAG, "Ambient pressure compensation skipped; no change required"); } } bool SCD4XComponent::update_ambient_pressure_compensation_(uint16_t pressure_in_hpa) { if (this->write_command(SCD4X_CMD_AMBIENT_PRESSURE_COMPENSATION, pressure_in_hpa)) { - ESP_LOGD(TAG, "setting ambient pressure compensation to %d hPa", pressure_in_hpa); + ESP_LOGD(TAG, "Setting ambient pressure compensation to %d hPa", pressure_in_hpa); return true; } else { - ESP_LOGE(TAG, "Error setting ambient pressure compensation."); + ESP_LOGE(TAG, "Error setting ambient pressure compensation"); return false; } } @@ -304,7 +311,7 @@ bool SCD4XComponent::start_measurement_() { static uint8_t remaining_retries = 3; while (remaining_retries) { if (!this->write_command(measurement_command)) { - ESP_LOGE(TAG, "Error starting measurements."); + ESP_LOGE(TAG, "Error starting measurements"); this->error_code_ = MEASUREMENT_INIT_FAILED; this->status_set_warning(); if (--remaining_retries == 0) diff --git a/esphome/components/scd4x/scd4x.h b/esphome/components/scd4x/scd4x.h index 22055e78d0..ab5d72aeec 100644 --- a/esphome/components/scd4x/scd4x.h +++ b/esphome/components/scd4x/scd4x.h @@ -8,18 +8,23 @@ namespace esphome { namespace scd4x { -enum ERRORCODE { +enum ErrorCode : uint8_t { COMMUNICATION_FAILED, SERIAL_NUMBER_IDENTIFICATION_FAILED, MEASUREMENT_INIT_FAILED, FRC_FAILED, - UNKNOWN + UNKNOWN, +}; + +enum MeasurementMode : uint8_t { + PERIODIC, + LOW_POWER_PERIODIC, + SINGLE_SHOT, + SINGLE_SHOT_RHT_ONLY, }; -enum MeasurementMode { PERIODIC, LOW_POWER_PERIODIC, SINGLE_SHOT, SINGLE_SHOT_RHT_ONLY }; class SCD4XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice { public: - float get_setup_priority() const override { return setup_priority::DATA; } void setup() override; void dump_config() override; void update() override; @@ -40,21 +45,18 @@ class SCD4XComponent : public PollingComponent, public sensirion_common::Sensiri protected: bool update_ambient_pressure_compensation_(uint16_t pressure_in_hpa); bool start_measurement_(); - ERRORCODE error_code_; - bool initialized_{false}; - - float temperature_offset_; - uint16_t altitude_compensation_; - bool ambient_pressure_compensation_; - uint16_t ambient_pressure_; - bool enable_asc_; - MeasurementMode measurement_mode_{PERIODIC}; sensor::Sensor *co2_sensor_{nullptr}; sensor::Sensor *temperature_sensor_{nullptr}; sensor::Sensor *humidity_sensor_{nullptr}; - // used for compensation - sensor::Sensor *ambient_pressure_source_{nullptr}; + sensor::Sensor *ambient_pressure_source_{nullptr}; // used for compensation + float temperature_offset_; + uint16_t altitude_compensation_{0}; + uint16_t ambient_pressure_{0}; // Per datasheet, valid values are 700 to 1200 hPa; 0 is a valid sentinel value + bool initialized_{false}; + bool enable_asc_{false}; + ErrorCode error_code_; + MeasurementMode measurement_mode_{PERIODIC}; }; } // namespace scd4x diff --git a/esphome/components/scd4x/sensor.py b/esphome/components/scd4x/sensor.py index f753f54c3b..fc859d63b8 100644 --- a/esphome/components/scd4x/sensor.py +++ b/esphome/components/scd4x/sensor.py @@ -4,9 +4,13 @@ import esphome.codegen as cg from esphome.components import i2c, sensirion_common, sensor import esphome.config_validation as cv from esphome.const import ( + CONF_AMBIENT_PRESSURE_COMPENSATION, + CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE, + CONF_AUTOMATIC_SELF_CALIBRATION, CONF_CO2, CONF_HUMIDITY, CONF_ID, + CONF_MEASUREMENT_MODE, CONF_TEMPERATURE, CONF_TEMPERATURE_OFFSET, CONF_VALUE, @@ -20,10 +24,6 @@ from esphome.const import ( UNIT_CELSIUS, UNIT_PARTS_PER_MILLION, UNIT_PERCENT, - CONF_AUTOMATIC_SELF_CALIBRATION, - CONF_AMBIENT_PRESSURE_COMPENSATION, - CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE, - CONF_MEASUREMENT_MODE, ) CODEOWNERS = ["@sjtrny", "@martgras"] diff --git a/esphome/components/script/script.h b/esphome/components/script/script.h index 165f90ed11..60175ec933 100644 --- a/esphome/components/script/script.h +++ b/esphome/components/script/script.h @@ -239,8 +239,6 @@ template class ScriptWaitAction : public Action, this->play_next_tuple_(this->var_); } - float get_setup_priority() const override { return setup_priority::DATA; } - void play(Ts... x) override { /* ignore - see play_complex */ } diff --git a/esphome/components/sdp3x/sensor.py b/esphome/components/sdp3x/sensor.py index 7cda2779ce..169ed374ed 100644 --- a/esphome/components/sdp3x/sensor.py +++ b/esphome/components/sdp3x/sensor.py @@ -2,10 +2,10 @@ import esphome.codegen as cg from esphome.components import i2c, sensirion_common, sensor import esphome.config_validation as cv from esphome.const import ( + CONF_MEASUREMENT_MODE, DEVICE_CLASS_PRESSURE, STATE_CLASS_MEASUREMENT, UNIT_HECTOPASCAL, - CONF_MEASUREMENT_MODE, ) DEPENDENCIES = ["i2c"] diff --git a/esphome/components/select/__init__.py b/esphome/components/select/__init__.py index e14a9351a0..ed1f6c020d 100644 --- a/esphome/components/select/__init__.py +++ b/esphome/components/select/__init__.py @@ -17,8 +17,8 @@ from esphome.const import ( CONF_WEB_SERVER, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@esphome/core"] IS_PLATFORM_COMPONENT = True @@ -65,6 +65,9 @@ _SELECT_SCHEMA = ( ) +_SELECT_SCHEMA.add_extra(entity_duplicate_validator("select")) + + def select_schema( class_: MockObjClass, *, @@ -89,7 +92,7 @@ SELECT_SCHEMA.add_extra(cv.deprecated_schema_constant("select")) async def setup_select_core_(var, config, *, options: list[str]): - await setup_entity(var, config) + await setup_entity(var, config, "select") cg.add(var.traits.set_options(options)) diff --git a/esphome/components/sen5x/sen5x.h b/esphome/components/sen5x/sen5x.h index 6d90636a89..0fa31605e6 100644 --- a/esphome/components/sen5x/sen5x.h +++ b/esphome/components/sen5x/sen5x.h @@ -48,7 +48,6 @@ struct TemperatureCompensation { class SEN5XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice { public: - float get_setup_priority() const override { return setup_priority::DATA; } void setup() override; void dump_config() override; void update() override; diff --git a/esphome/components/senseair/senseair.h b/esphome/components/senseair/senseair.h index bcec638f79..9f939d5b07 100644 --- a/esphome/components/senseair/senseair.h +++ b/esphome/components/senseair/senseair.h @@ -10,7 +10,6 @@ namespace senseair { class SenseAirComponent : public PollingComponent, public uart::UARTDevice { public: - float get_setup_priority() const override { return setup_priority::DATA; } void set_co2_sensor(sensor::Sensor *co2_sensor) { co2_sensor_ = co2_sensor; } void update() override; diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 1ad3cfabee..ea74361d51 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -101,8 +101,8 @@ from esphome.const import ( ENTITY_CATEGORY_CONFIG, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity from esphome.util import Registry CODEOWNERS = ["@esphome/core"] @@ -318,6 +318,8 @@ _SENSOR_SCHEMA = ( ) ) +_SENSOR_SCHEMA.add_extra(entity_duplicate_validator("sensor")) + def sensor_schema( class_: MockObjClass = cv.UNDEFINED, @@ -787,7 +789,7 @@ async def build_filters(config): async def setup_sensor_core_(var, config): - await setup_entity(var, config) + await setup_entity(var, config, "sensor") if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: cg.add(var.set_device_class(device_class)) diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index ce23c1f800..dd8635f0c0 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -118,7 +118,7 @@ optional QuantileFilter::new_value(float value) { size_t queue_size = quantile_queue.size(); if (queue_size) { size_t position = ceilf(queue_size * this->quantile_) - 1; - ESP_LOGVV(TAG, "QuantileFilter(%p)::position: %d/%d", this, position + 1, queue_size); + ESP_LOGVV(TAG, "QuantileFilter(%p)::position: %zu/%zu", this, position + 1, queue_size); result = quantile_queue[position]; } } diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index 6d6cff0400..7dab63b026 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -23,16 +23,22 @@ std::string state_class_to_string(StateClass state_class) { Sensor::Sensor() : state(NAN), raw_state(NAN) {} int8_t Sensor::get_accuracy_decimals() { - if (this->accuracy_decimals_.has_value()) - return *this->accuracy_decimals_; + if (this->sensor_flags_.has_accuracy_override) + return this->accuracy_decimals_; return 0; } -void Sensor::set_accuracy_decimals(int8_t accuracy_decimals) { this->accuracy_decimals_ = accuracy_decimals; } +void Sensor::set_accuracy_decimals(int8_t accuracy_decimals) { + this->accuracy_decimals_ = accuracy_decimals; + this->sensor_flags_.has_accuracy_override = true; +} -void Sensor::set_state_class(StateClass state_class) { this->state_class_ = state_class; } +void Sensor::set_state_class(StateClass state_class) { + this->state_class_ = state_class; + this->sensor_flags_.has_state_class_override = true; +} StateClass Sensor::get_state_class() { - if (this->state_class_.has_value()) - return *this->state_class_; + if (this->sensor_flags_.has_state_class_override) + return this->state_class_; return StateClass::STATE_CLASS_NONE; } diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index 456e876497..3fb6e5522b 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -80,9 +80,9 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa * state changes to the database when they are published, even if the state is the * same as before. */ - bool get_force_update() const { return force_update_; } + bool get_force_update() const { return sensor_flags_.force_update; } /// Set force update mode. - void set_force_update(bool force_update) { force_update_ = force_update; } + void set_force_update(bool force_update) { sensor_flags_.force_update = force_update; } /// Add a filter to the filter chain. Will be appended to the back. void add_filter(Filter *filter); @@ -155,9 +155,17 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa Filter *filter_list_{nullptr}; ///< Store all active filters. - optional accuracy_decimals_; ///< Accuracy in decimals override - optional state_class_{STATE_CLASS_NONE}; ///< State class override - bool force_update_{false}; ///< Force update mode + // Group small members together to avoid padding + int8_t accuracy_decimals_{-1}; ///< Accuracy in decimals (-1 = not set) + StateClass state_class_{STATE_CLASS_NONE}; ///< State class (STATE_CLASS_NONE = not set) + + // Bit-packed flags for sensor-specific settings + struct SensorFlags { + uint8_t has_accuracy_override : 1; + uint8_t has_state_class_override : 1; + uint8_t force_update : 1; + uint8_t reserved : 5; // Reserved for future use + } sensor_flags_{}; }; } // namespace sensor diff --git a/esphome/components/servo/servo.h b/esphome/components/servo/servo.h index 92d18bf601..ff1708dc53 100644 --- a/esphome/components/servo/servo.h +++ b/esphome/components/servo/servo.h @@ -20,7 +20,6 @@ class Servo : public Component { void detach(); void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_min_level(float min_level) { min_level_ = min_level; } void set_idle_level(float idle_level) { idle_level_ = idle_level; } void set_max_level(float max_level) { max_level_ = max_level; } diff --git a/esphome/components/sfa30/sfa30.h b/esphome/components/sfa30/sfa30.h index fa2c59f624..2b744b8da4 100644 --- a/esphome/components/sfa30/sfa30.h +++ b/esphome/components/sfa30/sfa30.h @@ -11,7 +11,6 @@ class SFA30Component : public PollingComponent, public sensirion_common::Sensiri enum ErrorCode { DEVICE_MARKING_READ_FAILED, MEASUREMENT_INIT_FAILED, UNKNOWN }; public: - float get_setup_priority() const override { return setup_priority::DATA; } void setup() override; void dump_config() override; void update() override; diff --git a/esphome/components/sgp30/sgp30.h b/esphome/components/sgp30/sgp30.h index 9e882e6b05..e6429a7bfa 100644 --- a/esphome/components/sgp30/sgp30.h +++ b/esphome/components/sgp30/sgp30.h @@ -32,7 +32,6 @@ class SGP30Component : public PollingComponent, public sensirion_common::Sensiri void setup() override; void update() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: void send_env_data_(); diff --git a/esphome/components/sgp4x/sgp4x.h b/esphome/components/sgp4x/sgp4x.h index 45ee66af68..8b31bca28c 100644 --- a/esphome/components/sgp4x/sgp4x.h +++ b/esphome/components/sgp4x/sgp4x.h @@ -75,7 +75,6 @@ class SGP4xComponent : public PollingComponent, public sensor::Sensor, public se void update() override; void take_sample(); void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_store_baseline(bool store_baseline) { store_baseline_ = store_baseline; } void set_voc_sensor(sensor::Sensor *voc_sensor) { voc_sensor_ = voc_sensor; } void set_nox_sensor(sensor::Sensor *nox_sensor) { nox_sensor_ = nox_sensor; } diff --git a/esphome/components/shelly_dimmer/stm32flash.cpp b/esphome/components/shelly_dimmer/stm32flash.cpp index 3871d89a2f..b052c0cee9 100644 --- a/esphome/components/shelly_dimmer/stm32flash.cpp +++ b/esphome/components/shelly_dimmer/stm32flash.cpp @@ -445,8 +445,7 @@ template stm32_err_t stm32_check_ack_timeout(const stm32_err_t s_err return STM32_ERR_OK; case STM32_ERR_NACK: log(); - // TODO: c++17 [[fallthrough]] - /* fallthrough */ + [[fallthrough]]; default: return STM32_ERR_UNKNOWN; } diff --git a/esphome/components/sht4x/sht4x.h b/esphome/components/sht4x/sht4x.h index 98e0629b50..accc7323be 100644 --- a/esphome/components/sht4x/sht4x.h +++ b/esphome/components/sht4x/sht4x.h @@ -17,7 +17,6 @@ enum SHT4XHEATERTIME : uint16_t { SHT4X_HEATERTIME_LONG = 1100, SHT4X_HEATERTIME class SHT4XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice { public: - float get_setup_priority() const override { return setup_priority::DATA; } void setup() override; void dump_config() override; void update() override; diff --git a/esphome/components/sm300d2/sm300d2.h b/esphome/components/sm300d2/sm300d2.h index 88c04e9813..4e97b54988 100644 --- a/esphome/components/sm300d2/sm300d2.h +++ b/esphome/components/sm300d2/sm300d2.h @@ -9,8 +9,6 @@ namespace sm300d2 { class SM300D2Sensor : public PollingComponent, public uart::UARTDevice { public: - float get_setup_priority() const override { return setup_priority::DATA; } - void set_co2_sensor(sensor::Sensor *co2_sensor) { co2_sensor_ = co2_sensor; } void set_formaldehyde_sensor(sensor::Sensor *formaldehyde_sensor) { formaldehyde_sensor_ = formaldehyde_sensor; } void set_tvoc_sensor(sensor::Sensor *tvoc_sensor) { tvoc_sensor_ = tvoc_sensor; } diff --git a/esphome/components/smt100/sensor.py b/esphome/components/smt100/sensor.py index ea42499379..f877ce2af0 100644 --- a/esphome/components/smt100/sensor.py +++ b/esphome/components/smt100/sensor.py @@ -6,6 +6,7 @@ from esphome.const import ( CONF_DIELECTRIC_CONSTANT, CONF_ID, CONF_MOISTURE, + CONF_PERMITTIVITY, CONF_TEMPERATURE, CONF_VOLTAGE, DEVICE_CLASS_TEMPERATURE, @@ -33,7 +34,10 @@ CONFIG_SCHEMA = ( accuracy_decimals=0, state_class=STATE_CLASS_MEASUREMENT, ), - cv.Optional(CONF_DIELECTRIC_CONSTANT): sensor.sensor_schema( + cv.Optional(CONF_DIELECTRIC_CONSTANT): cv.invalid( + "Use 'permittivity' instead" + ), + cv.Optional(CONF_PERMITTIVITY): sensor.sensor_schema( unit_of_measurement=UNIT_EMPTY, accuracy_decimals=2, state_class=STATE_CLASS_MEASUREMENT, @@ -76,9 +80,9 @@ async def to_code(config): sens = await sensor.new_sensor(config[CONF_COUNTS]) cg.add(var.set_counts_sensor(sens)) - if CONF_DIELECTRIC_CONSTANT in config: - sens = await sensor.new_sensor(config[CONF_DIELECTRIC_CONSTANT]) - cg.add(var.set_dielectric_constant_sensor(sens)) + if CONF_PERMITTIVITY in config: + sens = await sensor.new_sensor(config[CONF_PERMITTIVITY]) + cg.add(var.set_permittivity_sensor(sens)) if CONF_TEMPERATURE in config: sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) diff --git a/esphome/components/smt100/smt100.cpp b/esphome/components/smt100/smt100.cpp index 24ba05b894..c8dfb4c7bd 100644 --- a/esphome/components/smt100/smt100.cpp +++ b/esphome/components/smt100/smt100.cpp @@ -16,7 +16,7 @@ void SMT100Component::loop() { while (this->available() != 0) { if (readline_(read(), buffer, MAX_LINE_LENGTH) > 0) { int counts = (int) strtol((strtok(buffer, ",")), nullptr, 10); - float dielectric_constant = (float) strtod((strtok(nullptr, ",")), nullptr); + float permittivity = (float) strtod((strtok(nullptr, ",")), nullptr); float moisture = (float) strtod((strtok(nullptr, ",")), nullptr); float temperature = (float) strtod((strtok(nullptr, ",")), nullptr); float voltage = (float) strtod((strtok(nullptr, ",")), nullptr); @@ -25,8 +25,8 @@ void SMT100Component::loop() { counts_sensor_->publish_state(counts); } - if (this->dielectric_constant_sensor_ != nullptr) { - dielectric_constant_sensor_->publish_state(dielectric_constant); + if (this->permittivity_sensor_ != nullptr) { + permittivity_sensor_->publish_state(permittivity); } if (this->moisture_sensor_ != nullptr) { @@ -49,8 +49,8 @@ float SMT100Component::get_setup_priority() const { return setup_priority::DATA; void SMT100Component::dump_config() { ESP_LOGCONFIG(TAG, "SMT100:"); - LOG_SENSOR(TAG, "Counts", this->temperature_sensor_); - LOG_SENSOR(TAG, "Dielectric Constant", this->temperature_sensor_); + LOG_SENSOR(TAG, "Counts", this->counts_sensor_); + LOG_SENSOR(TAG, "Permittivity", this->permittivity_sensor_); LOG_SENSOR(TAG, "Temperature", this->temperature_sensor_); LOG_SENSOR(TAG, "Moisture", this->moisture_sensor_); LOG_UPDATE_INTERVAL(this); diff --git a/esphome/components/smt100/smt100.h b/esphome/components/smt100/smt100.h index 017818bdcf..86827607dc 100644 --- a/esphome/components/smt100/smt100.h +++ b/esphome/components/smt100/smt100.h @@ -20,8 +20,8 @@ class SMT100Component : public PollingComponent, public uart::UARTDevice { float get_setup_priority() const override; void set_counts_sensor(sensor::Sensor *counts_sensor) { this->counts_sensor_ = counts_sensor; } - void set_dielectric_constant_sensor(sensor::Sensor *dielectric_constant_sensor) { - this->dielectric_constant_sensor_ = dielectric_constant_sensor; + void set_permittivity_sensor(sensor::Sensor *permittivity_sensor) { + this->permittivity_sensor_ = permittivity_sensor; } void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; } void set_moisture_sensor(sensor::Sensor *moisture_sensor) { this->moisture_sensor_ = moisture_sensor; } @@ -31,7 +31,7 @@ class SMT100Component : public PollingComponent, public uart::UARTDevice { int readline_(int readch, char *buffer, int len); sensor::Sensor *counts_sensor_{nullptr}; - sensor::Sensor *dielectric_constant_sensor_{nullptr}; + sensor::Sensor *permittivity_sensor_{nullptr}; sensor::Sensor *moisture_sensor_{nullptr}; sensor::Sensor *temperature_sensor_{nullptr}; sensor::Sensor *voltage_sensor_{nullptr}; diff --git a/esphome/components/sn74hc595/__init__.py b/esphome/components/sn74hc595/__init__.py index db18b00cd1..26e5c03802 100644 --- a/esphome/components/sn74hc595/__init__.py +++ b/esphome/components/sn74hc595/__init__.py @@ -95,7 +95,7 @@ SN74HC595_PIN_SCHEMA = pins.gpio_base_schema( cv.int_range(min=0, max=2047), modes=[CONF_OUTPUT], mode_validator=_validate_output_mode, - invertable=True, + invertible=True, ).extend( { cv.Required(CONF_SN74HC595): cv.use_id(SN74HC595Component), diff --git a/esphome/components/sn74hc595/sn74hc595.cpp b/esphome/components/sn74hc595/sn74hc595.cpp index f8d24b898f..d8e33eec22 100644 --- a/esphome/components/sn74hc595/sn74hc595.cpp +++ b/esphome/components/sn74hc595/sn74hc595.cpp @@ -1,5 +1,6 @@ #include "sn74hc595.h" #include "esphome/core/log.h" +#include namespace esphome { namespace sn74hc595 { @@ -55,9 +56,9 @@ void SN74HC595Component::digital_write_(uint16_t pin, bool value) { } void SN74HC595GPIOComponent::write_gpio() { - for (auto byte = this->output_bytes_.rbegin(); byte != this->output_bytes_.rend(); byte++) { + for (uint8_t &output_byte : std::ranges::reverse_view(this->output_bytes_)) { for (int8_t i = 7; i >= 0; i--) { - bool bit = (*byte >> i) & 1; + bool bit = (output_byte >> i) & 1; this->data_pin_->digital_write(bit); this->clock_pin_->digital_write(true); this->clock_pin_->digital_write(false); @@ -68,9 +69,9 @@ void SN74HC595GPIOComponent::write_gpio() { #ifdef USE_SPI void SN74HC595SPIComponent::write_gpio() { - for (auto byte = this->output_bytes_.rbegin(); byte != this->output_bytes_.rend(); byte++) { + for (uint8_t &output_byte : std::ranges::reverse_view(this->output_bytes_)) { this->enable(); - this->transfer_byte(*byte); + this->transfer_byte(output_byte); this->disable(); } SN74HC595Component::write_gpio(); diff --git a/esphome/components/sntp/sntp_component.cpp b/esphome/components/sntp/sntp_component.cpp index f9a9981c52..d5839c1a2b 100644 --- a/esphome/components/sntp/sntp_component.cpp +++ b/esphome/components/sntp/sntp_component.cpp @@ -1,7 +1,7 @@ #include "sntp_component.h" #include "esphome/core/log.h" -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include "esp_sntp.h" #elif USE_ESP8266 #include "sntp.h" @@ -16,7 +16,7 @@ static const char *const TAG = "sntp"; void SNTPComponent::setup() { ESP_LOGCONFIG(TAG, "Running setup"); -#if defined(USE_ESP_IDF) +#if defined(USE_ESP32) if (esp_sntp_enabled()) { esp_sntp_stop(); } @@ -46,7 +46,7 @@ void SNTPComponent::dump_config() { } } void SNTPComponent::update() { -#if !defined(USE_ESP_IDF) +#if !defined(USE_ESP32) // force resync if (sntp_enabled()) { sntp_stop(); @@ -67,6 +67,12 @@ void SNTPComponent::loop() { time.minute, time.second); this->time_sync_callback_.call(); this->has_time_ = true; + +#ifdef USE_ESP_IDF + // On ESP-IDF, time sync is permanent and update() doesn't force resync + // Time is now synchronized, no need to check anymore + this->disable_loop(); +#endif } } // namespace sntp diff --git a/esphome/components/sntp/time.py b/esphome/components/sntp/time.py index 6f883d5bed..1c8ee402ad 100644 --- a/esphome/components/sntp/time.py +++ b/esphome/components/sntp/time.py @@ -7,6 +7,7 @@ from esphome.const import ( PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, + PLATFORM_LN882X, PLATFORM_RP2040, PLATFORM_RTL87XX, ) @@ -33,6 +34,7 @@ CONFIG_SCHEMA = cv.All( PLATFORM_ESP8266, PLATFORM_RP2040, PLATFORM_BK72XX, + PLATFORM_LN882X, PLATFORM_RTL87XX, ] ), diff --git a/esphome/components/socket/__init__.py b/esphome/components/socket/__init__.py index 667e30df4b..e085a09eac 100644 --- a/esphome/components/socket/__init__.py +++ b/esphome/components/socket/__init__.py @@ -1,5 +1,6 @@ import esphome.codegen as cg import esphome.config_validation as cv +from esphome.core import CORE CODEOWNERS = ["@esphome/core"] @@ -16,6 +17,7 @@ CONFIG_SCHEMA = cv.Schema( esp32=IMPLEMENTATION_BSD_SOCKETS, rp2040=IMPLEMENTATION_LWIP_TCP, bk72xx=IMPLEMENTATION_LWIP_SOCKETS, + ln882x=IMPLEMENTATION_LWIP_SOCKETS, rtl87xx=IMPLEMENTATION_LWIP_SOCKETS, host=IMPLEMENTATION_BSD_SOCKETS, ): cv.one_of( @@ -39,3 +41,18 @@ async def to_code(config): elif impl == IMPLEMENTATION_BSD_SOCKETS: cg.add_define("USE_SOCKET_IMPL_BSD_SOCKETS") cg.add_define("USE_SOCKET_SELECT_SUPPORT") + + +def FILTER_SOURCE_FILES() -> list[str]: + """Return list of socket implementation files that aren't selected by the user.""" + impl = CORE.config["socket"][CONF_IMPLEMENTATION] + + # Build list of files to exclude based on selected implementation + excluded = [] + if impl != IMPLEMENTATION_LWIP_TCP: + excluded.append("lwip_raw_tcp_impl.cpp") + if impl != IMPLEMENTATION_BSD_SOCKETS: + excluded.append("bsd_sockets_impl.cpp") + if impl != IMPLEMENTATION_LWIP_SOCKETS: + excluded.append("lwip_sockets_impl.cpp") + return excluded diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py index ffb5e11f79..58bfc3f411 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -13,6 +13,7 @@ from esphome.components.esp32.const import ( VARIANT_ESP32S2, VARIANT_ESP32S3, ) +from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( CONF_CLK_PIN, @@ -31,6 +32,7 @@ from esphome.const import ( PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040, + PlatformFramework, ) from esphome.core import CORE, coroutine_with_priority import esphome.final_validate as fv @@ -79,6 +81,7 @@ CONF_SPI_MODE = "spi_mode" CONF_FORCE_SW = "force_sw" CONF_INTERFACE = "interface" CONF_INTERFACE_INDEX = "interface_index" +CONF_RELEASE_DEVICE = "release_device" TYPE_SINGLE = "single" TYPE_QUAD = "quad" TYPE_OCTAL = "octal" @@ -378,6 +381,7 @@ def spi_device_schema( cv.Optional(CONF_SPI_MODE, default=default_mode): cv.enum( SPI_MODE_OPTIONS, upper=True ), + cv.Optional(CONF_RELEASE_DEVICE): cv.All(cv.boolean, cv.only_with_esp_idf), } if cs_pin_required: schema[cv.Required(CONF_CS_PIN)] = pins.gpio_output_pin_schema @@ -389,13 +393,15 @@ def spi_device_schema( async def register_spi_device(var, config): parent = await cg.get_variable(config[CONF_SPI_ID]) cg.add(var.set_spi_parent(parent)) - if CONF_CS_PIN in config: - pin = await cg.gpio_pin_expression(config[CONF_CS_PIN]) + if cs_pin := config.get(CONF_CS_PIN): + pin = await cg.gpio_pin_expression(cs_pin) cg.add(var.set_cs_pin(pin)) - if CONF_DATA_RATE in config: - cg.add(var.set_data_rate(config[CONF_DATA_RATE])) - if CONF_SPI_MODE in config: - cg.add(var.set_mode(config[CONF_SPI_MODE])) + if data_rate := config.get(CONF_DATA_RATE): + cg.add(var.set_data_rate(data_rate)) + if spi_mode := config.get(CONF_SPI_MODE): + cg.add(var.set_mode(spi_mode)) + if release_device := config.get(CONF_RELEASE_DEVICE): + cg.add(var.set_release_device(release_device)) def final_validate_device_schema(name: str, *, require_mosi: bool, require_miso: bool): @@ -419,3 +425,18 @@ def final_validate_device_schema(name: str, *, require_mosi: bool, require_miso: {cv.Required(CONF_SPI_ID): fv.id_declaration_match_schema(hub_schema)}, extra=cv.ALLOW_EXTRA, ) + + +FILTER_SOURCE_FILES = filter_source_files_from_platform( + { + "spi_arduino.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP8266_ARDUINO, + PlatformFramework.RP2040_ARDUINO, + PlatformFramework.BK72XX_ARDUINO, + PlatformFramework.RTL87XX_ARDUINO, + PlatformFramework.LN882X_ARDUINO, + }, + "spi_esp_idf.cpp": {PlatformFramework.ESP32_IDF}, + } +) diff --git a/esphome/components/spi/spi.cpp b/esphome/components/spi/spi.cpp index 76d9d8ae86..805a774ceb 100644 --- a/esphome/components/spi/spi.cpp +++ b/esphome/components/spi/spi.cpp @@ -16,12 +16,13 @@ bool SPIDelegate::is_ready() { return true; } GPIOPin *const NullPin::NULL_PIN = new NullPin(); // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) SPIDelegate *SPIComponent::register_device(SPIClient *device, SPIMode mode, SPIBitOrder bit_order, uint32_t data_rate, - GPIOPin *cs_pin) { + GPIOPin *cs_pin, bool release_device, bool write_only) { if (this->devices_.count(device) != 0) { ESP_LOGE(TAG, "Device already registered"); return this->devices_[device]; } - SPIDelegate *delegate = this->spi_bus_->get_delegate(data_rate, bit_order, mode, cs_pin); // NOLINT + SPIDelegate *delegate = + this->spi_bus_->get_delegate(data_rate, bit_order, mode, cs_pin, release_device, write_only); // NOLINT this->devices_[device] = delegate; return delegate; } diff --git a/esphome/components/spi/spi.h b/esphome/components/spi/spi.h index f96d3da251..5bc80350da 100644 --- a/esphome/components/spi/spi.h +++ b/esphome/components/spi/spi.h @@ -317,7 +317,8 @@ class SPIBus { SPIBus(GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi) : clk_pin_(clk), sdo_pin_(sdo), sdi_pin_(sdi) {} - virtual SPIDelegate *get_delegate(uint32_t data_rate, SPIBitOrder bit_order, SPIMode mode, GPIOPin *cs_pin) { + virtual SPIDelegate *get_delegate(uint32_t data_rate, SPIBitOrder bit_order, SPIMode mode, GPIOPin *cs_pin, + bool release_device, bool write_only) { return new SPIDelegateBitBash(data_rate, bit_order, mode, cs_pin, this->clk_pin_, this->sdo_pin_, this->sdi_pin_); } @@ -334,7 +335,7 @@ class SPIClient; class SPIComponent : public Component { public: SPIDelegate *register_device(SPIClient *device, SPIMode mode, SPIBitOrder bit_order, uint32_t data_rate, - GPIOPin *cs_pin); + GPIOPin *cs_pin, bool release_device, bool write_only); void unregister_device(SPIClient *device); void set_clk(GPIOPin *clk) { this->clk_pin_ = clk; } @@ -390,7 +391,8 @@ class SPIClient { virtual void spi_setup() { esph_log_d("spi_device", "mode %u, data_rate %ukHz", (unsigned) this->mode_, (unsigned) (this->data_rate_ / 1000)); - this->delegate_ = this->parent_->register_device(this, this->mode_, this->bit_order_, this->data_rate_, this->cs_); + this->delegate_ = this->parent_->register_device(this, this->mode_, this->bit_order_, this->data_rate_, this->cs_, + this->release_device_, this->write_only_); } virtual void spi_teardown() { @@ -399,6 +401,8 @@ class SPIClient { } bool spi_is_ready() { return this->delegate_->is_ready(); } + void set_release_device(bool release) { this->release_device_ = release; } + void set_write_only(bool write_only) { this->write_only_ = write_only; } protected: SPIBitOrder bit_order_{BIT_ORDER_MSB_FIRST}; @@ -406,6 +410,8 @@ class SPIClient { uint32_t data_rate_{1000000}; SPIComponent *parent_{nullptr}; GPIOPin *cs_{nullptr}; + bool release_device_{false}; + bool write_only_{false}; SPIDelegate *delegate_{SPIDelegate::NULL_DELEGATE}; }; diff --git a/esphome/components/spi/spi_arduino.cpp b/esphome/components/spi/spi_arduino.cpp index 432f7cf2cd..a34e3c3c82 100644 --- a/esphome/components/spi/spi_arduino.cpp +++ b/esphome/components/spi/spi_arduino.cpp @@ -43,10 +43,7 @@ class SPIDelegateHw : public SPIDelegate { return; } #ifdef USE_RP2040 - // avoid overwriting the supplied buffer. Use vector for automatic deallocation - auto rxbuf = std::vector(length); - memcpy(rxbuf.data(), ptr, length); - this->channel_->transfer((void *) rxbuf.data(), length); + this->channel_->transfer(ptr, nullptr, length); #elif defined(USE_ESP8266) // ESP8266 SPI library requires the pointer to be word aligned, but the data may not be // so we need to copy the data to a temporary buffer @@ -89,7 +86,8 @@ class SPIBusHw : public SPIBus { #endif } - SPIDelegate *get_delegate(uint32_t data_rate, SPIBitOrder bit_order, SPIMode mode, GPIOPin *cs_pin) override { + SPIDelegate *get_delegate(uint32_t data_rate, SPIBitOrder bit_order, SPIMode mode, GPIOPin *cs_pin, + bool release_device, bool write_only) override { return new SPIDelegateHw(this->channel_, data_rate, bit_order, mode, cs_pin); } diff --git a/esphome/components/spi/spi_esp_idf.cpp b/esphome/components/spi/spi_esp_idf.cpp index a78da2cd9a..549f516eb1 100644 --- a/esphome/components/spi/spi_esp_idf.cpp +++ b/esphome/components/spi/spi_esp_idf.cpp @@ -11,34 +11,26 @@ static const size_t MAX_TRANSFER_SIZE = 4092; // dictated by ESP-IDF API. class SPIDelegateHw : public SPIDelegate { public: SPIDelegateHw(SPIInterface channel, uint32_t data_rate, SPIBitOrder bit_order, SPIMode mode, GPIOPin *cs_pin, - bool write_only) - : SPIDelegate(data_rate, bit_order, mode, cs_pin), channel_(channel), write_only_(write_only) { - spi_device_interface_config_t config = {}; - config.mode = static_cast(mode); - config.clock_speed_hz = static_cast(data_rate); - config.spics_io_num = -1; - config.flags = 0; - config.queue_size = 1; - config.pre_cb = nullptr; - config.post_cb = nullptr; - if (bit_order == BIT_ORDER_LSB_FIRST) - config.flags |= SPI_DEVICE_BIT_LSBFIRST; - if (write_only) - config.flags |= SPI_DEVICE_HALFDUPLEX | SPI_DEVICE_NO_DUMMY; - esp_err_t const err = spi_bus_add_device(channel, &config, &this->handle_); - if (err != ESP_OK) - ESP_LOGE(TAG, "Add device failed - err %X", err); + bool release_device, bool write_only) + : SPIDelegate(data_rate, bit_order, mode, cs_pin), + channel_(channel), + release_device_(release_device), + write_only_(write_only) { + if (!this->release_device_) + add_device_(); } bool is_ready() override { return this->handle_ != nullptr; } void begin_transaction() override { + if (this->release_device_) + this->add_device_(); if (this->is_ready()) { if (spi_device_acquire_bus(this->handle_, portMAX_DELAY) != ESP_OK) ESP_LOGE(TAG, "Failed to acquire SPI bus"); SPIDelegate::begin_transaction(); } else { - ESP_LOGW(TAG, "spi_setup called before initialisation"); + ESP_LOGW(TAG, "SPI device not ready, cannot begin transaction"); } } @@ -46,6 +38,10 @@ class SPIDelegateHw : public SPIDelegate { if (this->is_ready()) { SPIDelegate::end_transaction(); spi_device_release_bus(this->handle_); + if (this->release_device_) { + spi_bus_remove_device(this->handle_); + this->handle_ = nullptr; // reset handle to indicate no device is registered + } } } @@ -189,8 +185,30 @@ class SPIDelegateHw : public SPIDelegate { void read_array(uint8_t *ptr, size_t length) override { this->transfer(nullptr, ptr, length); } protected: + bool add_device_() { + spi_device_interface_config_t config = {}; + config.mode = static_cast(this->mode_); + config.clock_speed_hz = static_cast(this->data_rate_); + config.spics_io_num = -1; + config.flags = 0; + config.queue_size = 1; + config.pre_cb = nullptr; + config.post_cb = nullptr; + if (this->bit_order_ == BIT_ORDER_LSB_FIRST) + config.flags |= SPI_DEVICE_BIT_LSBFIRST; + if (this->write_only_) + config.flags |= SPI_DEVICE_HALFDUPLEX | SPI_DEVICE_NO_DUMMY; + esp_err_t const err = spi_bus_add_device(this->channel_, &config, &this->handle_); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Add device failed - err %X", err); + return false; + } + return true; + } + SPIInterface channel_{}; spi_device_handle_t handle_{}; + bool release_device_{false}; bool write_only_{false}; }; @@ -231,9 +249,10 @@ class SPIBusHw : public SPIBus { ESP_LOGE(TAG, "Bus init failed - err %X", err); } - SPIDelegate *get_delegate(uint32_t data_rate, SPIBitOrder bit_order, SPIMode mode, GPIOPin *cs_pin) override { - return new SPIDelegateHw(this->channel_, data_rate, bit_order, mode, cs_pin, - Utility::get_pin_no(this->sdi_pin_) == -1); + SPIDelegate *get_delegate(uint32_t data_rate, SPIBitOrder bit_order, SPIMode mode, GPIOPin *cs_pin, + bool release_device, bool write_only) override { + return new SPIDelegateHw(this->channel_, data_rate, bit_order, mode, cs_pin, release_device, + write_only || Utility::get_pin_no(this->sdi_pin_) == -1); } protected: diff --git a/esphome/components/spi_led_strip/spi_led_strip.cpp b/esphome/components/spi_led_strip/spi_led_strip.cpp index 46243c0686..85c10ee87d 100644 --- a/esphome/components/spi_led_strip/spi_led_strip.cpp +++ b/esphome/components/spi_led_strip/spi_led_strip.cpp @@ -5,7 +5,7 @@ namespace spi_led_strip { SpiLedStrip::SpiLedStrip(uint16_t num_leds) { this->num_leds_ = num_leds; - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator allocator; this->buffer_size_ = num_leds * 4 + 8; this->buf_ = allocator.allocate(this->buffer_size_); if (this->buf_ == nullptr) { diff --git a/esphome/components/sps30/sps30.h b/esphome/components/sps30/sps30.h index cf2e7a7d4f..04189247e8 100644 --- a/esphome/components/sps30/sps30.h +++ b/esphome/components/sps30/sps30.h @@ -26,7 +26,6 @@ class SPS30Component : public PollingComponent, public sensirion_common::Sensiri void setup() override; void update() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } bool start_fan_cleaning(); diff --git a/esphome/components/ssd1306_base/ssd1306_base.cpp b/esphome/components/ssd1306_base/ssd1306_base.cpp index 0547a77184..1f039cff78 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.cpp +++ b/esphome/components/ssd1306_base/ssd1306_base.cpp @@ -224,7 +224,7 @@ bool SSD1306::is_sh1106_() const { } bool SSD1306::is_sh1107_() const { return this->model_ == SH1107_MODEL_128_64 || this->model_ == SH1107_MODEL_128_128; } bool SSD1306::is_ssd1305_() const { - return this->model_ == SSD1305_MODEL_128_64 || this->model_ == SSD1305_MODEL_128_64; + return this->model_ == SSD1305_MODEL_128_64 || this->model_ == SSD1305_MODEL_128_32; } void SSD1306::update() { this->do_update_(); diff --git a/esphome/components/st7701s/st7701s.cpp b/esphome/components/st7701s/st7701s.cpp index 6261f33b77..46509a7f9f 100644 --- a/esphome/components/st7701s/st7701s.cpp +++ b/esphome/components/st7701s/st7701s.cpp @@ -12,10 +12,8 @@ void ST7701S::setup() { esp_lcd_rgb_panel_config_t config{}; config.flags.fb_in_psram = 1; -#if ESP_IDF_VERSION_MAJOR >= 5 config.bounce_buffer_size_px = this->width_ * 10; config.num_fbs = 1; -#endif // ESP_IDF_VERSION_MAJOR config.timings.h_res = this->width_; config.timings.v_res = this->height_; config.timings.hsync_pulse_width = this->hsync_pulse_width_; @@ -48,10 +46,8 @@ void ST7701S::setup() { } void ST7701S::loop() { -#if ESP_IDF_VERSION_MAJOR >= 5 if (this->handle_ != nullptr) esp_lcd_rgb_panel_restart(this->handle_); -#endif // ESP_IDF_VERSION_MAJOR } void ST7701S::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order, diff --git a/esphome/components/status/status_binary_sensor.h b/esphome/components/status/status_binary_sensor.h index 08aa0fb32f..feda8b6328 100644 --- a/esphome/components/status/status_binary_sensor.h +++ b/esphome/components/status/status_binary_sensor.h @@ -13,8 +13,6 @@ class StatusBinarySensor : public binary_sensor::BinarySensor, public Component void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } - bool is_status_binary_sensor() const override { return true; } }; diff --git a/esphome/components/substitutions/__init__.py b/esphome/components/substitutions/__init__.py index 41e49f70db..5878af43b2 100644 --- a/esphome/components/substitutions/__init__.py +++ b/esphome/components/substitutions/__init__.py @@ -5,6 +5,13 @@ from esphome.config_helpers import Extend, Remove, merge_config import esphome.config_validation as cv from esphome.const import CONF_SUBSTITUTIONS, VALID_SUBSTITUTIONS_CHARACTERS from esphome.yaml_util import ESPHomeDataBase, make_data_base +from .jinja import ( + Jinja, + JinjaStr, + has_jinja, + TemplateError, + TemplateRuntimeError, +) CODEOWNERS = ["@esphome/core"] _LOGGER = logging.getLogger(__name__) @@ -28,7 +35,7 @@ def validate_substitution_key(value): CONFIG_SCHEMA = cv.Schema( { - validate_substitution_key: cv.string_strict, + validate_substitution_key: object, } ) @@ -37,7 +44,42 @@ async def to_code(config): pass -def _expand_substitutions(substitutions, value, path, ignore_missing): +def _expand_jinja(value, orig_value, path, jinja, ignore_missing): + if has_jinja(value): + # If the original value passed in to this function is a JinjaStr, it means it contains an unresolved + # Jinja expression from a previous pass. + if isinstance(orig_value, JinjaStr): + # Rebuild the JinjaStr in case it was lost while replacing substitutions. + value = JinjaStr(value, orig_value.upvalues) + try: + # Invoke the jinja engine to evaluate the expression. + value, err = jinja.expand(value) + if err is not None: + if not ignore_missing and "password" not in path: + _LOGGER.warning( + "Found '%s' (see %s) which looks like an expression," + " but could not resolve all the variables: %s", + value, + "->".join(str(x) for x in path), + err.message, + ) + except ( + TemplateError, + TemplateRuntimeError, + RuntimeError, + ArithmeticError, + AttributeError, + TypeError, + ) as err: + raise cv.Invalid( + f"{type(err).__name__} Error evaluating jinja expression '{value}': {str(err)}." + f" See {'->'.join(str(x) for x in path)}", + path, + ) + return value + + +def _expand_substitutions(substitutions, value, path, jinja, ignore_missing): if "$" not in value: return value @@ -47,7 +89,8 @@ def _expand_substitutions(substitutions, value, path, ignore_missing): while True: m = cv.VARIABLE_PROG.search(value, i) if not m: - # Nothing more to match. Done + # No more variable substitutions found. See if the remainder looks like a jinja template + value = _expand_jinja(value, orig_value, path, jinja, ignore_missing) break i, j = m.span(0) @@ -67,8 +110,15 @@ def _expand_substitutions(substitutions, value, path, ignore_missing): continue sub = substitutions[name] + + if i == 0 and j == len(value): + # The variable spans the whole expression, e.g., "${varName}". Return its resolved value directly + # to conserve its type. + value = sub + break + tail = value[j:] - value = value[:i] + sub + value = value[:i] + str(sub) i = len(value) value += tail @@ -77,36 +127,40 @@ def _expand_substitutions(substitutions, value, path, ignore_missing): if isinstance(orig_value, ESPHomeDataBase): # even though string can get larger or smaller, the range should point # to original document marks - return make_data_base(value, orig_value) + value = make_data_base(value, orig_value) return value -def _substitute_item(substitutions, item, path, ignore_missing): +def _substitute_item(substitutions, item, path, jinja, ignore_missing): if isinstance(item, list): for i, it in enumerate(item): - sub = _substitute_item(substitutions, it, path + [i], ignore_missing) + sub = _substitute_item(substitutions, it, path + [i], jinja, ignore_missing) if sub is not None: item[i] = sub elif isinstance(item, dict): replace_keys = [] for k, v in item.items(): if path or k != CONF_SUBSTITUTIONS: - sub = _substitute_item(substitutions, k, path + [k], ignore_missing) + sub = _substitute_item( + substitutions, k, path + [k], jinja, ignore_missing + ) if sub is not None: replace_keys.append((k, sub)) - sub = _substitute_item(substitutions, v, path + [k], ignore_missing) + sub = _substitute_item(substitutions, v, path + [k], jinja, ignore_missing) if sub is not None: item[k] = sub for old, new in replace_keys: item[new] = merge_config(item.get(old), item.get(new)) del item[old] elif isinstance(item, str): - sub = _expand_substitutions(substitutions, item, path, ignore_missing) - if sub != item: + sub = _expand_substitutions(substitutions, item, path, jinja, ignore_missing) + if isinstance(sub, JinjaStr) or sub != item: return sub elif isinstance(item, (core.Lambda, Extend, Remove)): - sub = _expand_substitutions(substitutions, item.value, path, ignore_missing) + sub = _expand_substitutions( + substitutions, item.value, path, jinja, ignore_missing + ) if sub != item: item.value = sub return None @@ -116,11 +170,11 @@ def do_substitution_pass(config, command_line_substitutions, ignore_missing=Fals if CONF_SUBSTITUTIONS not in config and not command_line_substitutions: return - substitutions = config.get(CONF_SUBSTITUTIONS) - if substitutions is None: - substitutions = command_line_substitutions - elif command_line_substitutions: - substitutions = {**substitutions, **command_line_substitutions} + # Merge substitutions in config, overriding with substitutions coming from command line: + substitutions = { + **config.get(CONF_SUBSTITUTIONS, {}), + **(command_line_substitutions or {}), + } with cv.prepend_path("substitutions"): if not isinstance(substitutions, dict): raise cv.Invalid( @@ -133,7 +187,7 @@ def do_substitution_pass(config, command_line_substitutions, ignore_missing=Fals sub = validate_substitution_key(key) if sub != key: replace_keys.append((key, sub)) - substitutions[key] = cv.string_strict(value) + substitutions[key] = value for old, new in replace_keys: substitutions[new] = substitutions[old] del substitutions[old] @@ -141,4 +195,7 @@ def do_substitution_pass(config, command_line_substitutions, ignore_missing=Fals config[CONF_SUBSTITUTIONS] = substitutions # Move substitutions to the first place to replace substitutions in them correctly config.move_to_end(CONF_SUBSTITUTIONS, False) - _substitute_item(substitutions, config, [], ignore_missing) + + # Create a Jinja environment that will consider substitutions in scope: + jinja = Jinja(substitutions) + _substitute_item(substitutions, config, [], jinja, ignore_missing) diff --git a/esphome/components/substitutions/jinja.py b/esphome/components/substitutions/jinja.py new file mode 100644 index 0000000000..9ecdbab844 --- /dev/null +++ b/esphome/components/substitutions/jinja.py @@ -0,0 +1,99 @@ +import logging +import math +import re +import jinja2 as jinja +from jinja2.nativetypes import NativeEnvironment + +TemplateError = jinja.TemplateError +TemplateSyntaxError = jinja.TemplateSyntaxError +TemplateRuntimeError = jinja.TemplateRuntimeError +UndefinedError = jinja.UndefinedError +Undefined = jinja.Undefined + +_LOGGER = logging.getLogger(__name__) + +DETECT_JINJA = r"(\$\{)" +detect_jinja_re = re.compile( + r"<%.+?%>" # Block form expression: <% ... %> + r"|\$\{[^}]+\}", # Braced form expression: ${ ... } + flags=re.MULTILINE, +) + + +def has_jinja(st): + return detect_jinja_re.search(st) is not None + + +class JinjaStr(str): + """ + Wraps a string containing an unresolved Jinja expression, + storing the variables visible to it when it failed to resolve. + For example, an expression inside a package, `${ A * B }` may fail + to resolve at package parsing time if `A` is a local package var + but `B` is a substitution defined in the root yaml. + Therefore, we store the value of `A` as an upvalue bound + to the original string so we may be able to resolve `${ A * B }` + later in the main substitutions pass. + """ + + def __new__(cls, value: str, upvalues=None): + obj = super().__new__(cls, value) + obj.upvalues = upvalues or {} + return obj + + def __init__(self, value: str, upvalues=None): + self.upvalues = upvalues or {} + + +class Jinja: + """ + Wraps a Jinja environment + """ + + def __init__(self, context_vars): + self.env = NativeEnvironment( + trim_blocks=True, + lstrip_blocks=True, + block_start_string="<%", + block_end_string="%>", + line_statement_prefix="#", + line_comment_prefix="##", + variable_start_string="${", + variable_end_string="}", + undefined=jinja.StrictUndefined, + ) + self.env.add_extension("jinja2.ext.do") + self.env.globals["math"] = math # Inject entire math module + self.context_vars = {**context_vars} + self.env.globals = {**self.env.globals, **self.context_vars} + + def expand(self, content_str): + """ + Renders a string that may contain Jinja expressions or statements + Returns the resulting processed string if all values could be resolved. + Otherwise, it returns a tagged (JinjaStr) string that captures variables + in scope (upvalues), like a closure for later evaluation. + """ + result = None + override_vars = {} + if isinstance(content_str, JinjaStr): + # If `value` is already a JinjaStr, it means we are trying to evaluate it again + # in a parent pass. + # Hopefully, all required variables are visible now. + override_vars = content_str.upvalues + try: + template = self.env.from_string(content_str) + result = template.render(override_vars) + if isinstance(result, Undefined): + # This happens when the expression is simply an undefined variable. Jinja does not + # raise an exception, instead we get "Undefined". + # Trigger an UndefinedError exception so we skip to below. + print("" + result) + except (TemplateSyntaxError, UndefinedError) as err: + # `content_str` contains a Jinja expression that refers to a variable that is undefined + # in this scope. Perhaps it refers to a root substitution that is not visible yet. + # Therefore, return the original `content_str` as a JinjaStr, which contains the variables + # that are actually visible to it at this point to postpone evaluation. + return JinjaStr(content_str, {**self.context_vars, **override_vars}), err + + return result, None diff --git a/esphome/components/sun/sun.cpp b/esphome/components/sun/sun.cpp index 2fd9394a5e..df7030461b 100644 --- a/esphome/components/sun/sun.cpp +++ b/esphome/components/sun/sun.cpp @@ -1,5 +1,6 @@ #include "sun.h" #include "esphome/core/log.h" +#include /* The formulas/algorithms in this module are based on the book @@ -18,14 +19,12 @@ using namespace esphome::sun::internal; static const char *const TAG = "sun"; -#undef PI #undef degrees #undef radians #undef sq -static const num_t PI = 3.141592653589793; -inline num_t degrees(num_t rad) { return rad * 180 / PI; } -inline num_t radians(num_t deg) { return deg * PI / 180; } +inline num_t degrees(num_t rad) { return rad * 180 / std::numbers::pi; } +inline num_t radians(num_t deg) { return deg * std::numbers::pi / 180; } inline num_t arcdeg(num_t deg, num_t minutes, num_t seconds) { return deg + minutes / 60 + seconds / 3600; } inline num_t sq(num_t x) { return x * x; } inline num_t cb(num_t x) { return x * x * x; } diff --git a/esphome/components/switch/__init__.py b/esphome/components/switch/__init__.py index 0211c648fc..c09675069f 100644 --- a/esphome/components/switch/__init__.py +++ b/esphome/components/switch/__init__.py @@ -20,8 +20,8 @@ from esphome.const import ( DEVICE_CLASS_SWITCH, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@esphome/core"] IS_PLATFORM_COMPONENT = True @@ -91,6 +91,9 @@ _SWITCH_SCHEMA = ( ) +_SWITCH_SCHEMA.add_extra(entity_duplicate_validator("switch")) + + def switch_schema( class_: MockObjClass, *, @@ -131,7 +134,7 @@ SWITCH_SCHEMA.add_extra(cv.deprecated_schema_constant("switch")) async def setup_switch_core_(var, config): - await setup_entity(var, config) + await setup_entity(var, config, "switch") if (inverted := config.get(CONF_INVERTED)) is not None: cg.add(var.set_inverted(inverted)) diff --git a/esphome/components/switch/binary_sensor/switch_binary_sensor.h b/esphome/components/switch/binary_sensor/switch_binary_sensor.h index 5a947c2fb4..53b07da903 100644 --- a/esphome/components/switch/binary_sensor/switch_binary_sensor.h +++ b/esphome/components/switch/binary_sensor/switch_binary_sensor.h @@ -12,7 +12,6 @@ class SwitchBinarySensor : public binary_sensor::BinarySensor, public Component void set_source(Switch *source) { source_ = source; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: Switch *source_; diff --git a/esphome/components/sx126x/__init__.py b/esphome/components/sx126x/__init__.py new file mode 100644 index 0000000000..492febe283 --- /dev/null +++ b/esphome/components/sx126x/__init__.py @@ -0,0 +1,317 @@ +from esphome import automation, pins +import esphome.codegen as cg +from esphome.components import spi +import esphome.config_validation as cv +from esphome.const import CONF_BUSY_PIN, CONF_DATA, CONF_FREQUENCY, CONF_ID +from esphome.core import TimePeriod + +MULTI_CONF = True +CODEOWNERS = ["@swoboda1337"] +DEPENDENCIES = ["spi"] + +CONF_SX126X_ID = "sx126x_id" + +CONF_BANDWIDTH = "bandwidth" +CONF_BITRATE = "bitrate" +CONF_CODING_RATE = "coding_rate" +CONF_CRC_ENABLE = "crc_enable" +CONF_DEVIATION = "deviation" +CONF_DIO1_PIN = "dio1_pin" +CONF_HW_VERSION = "hw_version" +CONF_MODULATION = "modulation" +CONF_ON_PACKET = "on_packet" +CONF_PA_POWER = "pa_power" +CONF_PA_RAMP = "pa_ramp" +CONF_PAYLOAD_LENGTH = "payload_length" +CONF_PREAMBLE_DETECT = "preamble_detect" +CONF_PREAMBLE_SIZE = "preamble_size" +CONF_RST_PIN = "rst_pin" +CONF_RX_START = "rx_start" +CONF_RF_SWITCH = "rf_switch" +CONF_SHAPING = "shaping" +CONF_SPREADING_FACTOR = "spreading_factor" +CONF_SYNC_VALUE = "sync_value" +CONF_TCXO_VOLTAGE = "tcxo_voltage" +CONF_TCXO_DELAY = "tcxo_delay" + +sx126x_ns = cg.esphome_ns.namespace("sx126x") +SX126x = sx126x_ns.class_("SX126x", cg.Component, spi.SPIDevice) +SX126xListener = sx126x_ns.class_("SX126xListener") +SX126xBw = sx126x_ns.enum("SX126xBw") +SX126xPacketType = sx126x_ns.enum("SX126xPacketType") +SX126xTcxoCtrl = sx126x_ns.enum("SX126xTcxoCtrl") +SX126xRampTime = sx126x_ns.enum("SX126xRampTime") +SX126xPulseShape = sx126x_ns.enum("SX126xPulseShape") +SX126xLoraCr = sx126x_ns.enum("SX126xLoraCr") + +BW = { + "4_8kHz": SX126xBw.SX126X_BW_4800, + "5_8kHz": SX126xBw.SX126X_BW_5800, + "7_3kHz": SX126xBw.SX126X_BW_7300, + "9_7kHz": SX126xBw.SX126X_BW_9700, + "11_7kHz": SX126xBw.SX126X_BW_11700, + "14_6kHz": SX126xBw.SX126X_BW_14600, + "19_5kHz": SX126xBw.SX126X_BW_19500, + "23_4kHz": SX126xBw.SX126X_BW_23400, + "29_3kHz": SX126xBw.SX126X_BW_29300, + "39_0kHz": SX126xBw.SX126X_BW_39000, + "46_9kHz": SX126xBw.SX126X_BW_46900, + "58_6kHz": SX126xBw.SX126X_BW_58600, + "78_2kHz": SX126xBw.SX126X_BW_78200, + "93_8kHz": SX126xBw.SX126X_BW_93800, + "117_3kHz": SX126xBw.SX126X_BW_117300, + "156_2kHz": SX126xBw.SX126X_BW_156200, + "187_2kHz": SX126xBw.SX126X_BW_187200, + "234_3kHz": SX126xBw.SX126X_BW_234300, + "312_0kHz": SX126xBw.SX126X_BW_312000, + "373_6kHz": SX126xBw.SX126X_BW_373600, + "467_0kHz": SX126xBw.SX126X_BW_467000, + "7_8kHz": SX126xBw.SX126X_BW_7810, + "10_4kHz": SX126xBw.SX126X_BW_10420, + "15_6kHz": SX126xBw.SX126X_BW_15630, + "20_8kHz": SX126xBw.SX126X_BW_20830, + "31_3kHz": SX126xBw.SX126X_BW_31250, + "41_7kHz": SX126xBw.SX126X_BW_41670, + "62_5kHz": SX126xBw.SX126X_BW_62500, + "125_0kHz": SX126xBw.SX126X_BW_125000, + "250_0kHz": SX126xBw.SX126X_BW_250000, + "500_0kHz": SX126xBw.SX126X_BW_500000, +} + +CODING_RATE = { + "CR_4_5": SX126xLoraCr.LORA_CR_4_5, + "CR_4_6": SX126xLoraCr.LORA_CR_4_6, + "CR_4_7": SX126xLoraCr.LORA_CR_4_7, + "CR_4_8": SX126xLoraCr.LORA_CR_4_8, +} + +MOD = { + "LORA": SX126xPacketType.PACKET_TYPE_LORA, + "FSK": SX126xPacketType.PACKET_TYPE_GFSK, +} + +TCXO_VOLTAGE = { + "1_6V": SX126xTcxoCtrl.TCXO_CTRL_1_6V, + "1_7V": SX126xTcxoCtrl.TCXO_CTRL_1_7V, + "1_8V": SX126xTcxoCtrl.TCXO_CTRL_1_8V, + "2_2V": SX126xTcxoCtrl.TCXO_CTRL_2_2V, + "2_4V": SX126xTcxoCtrl.TCXO_CTRL_2_4V, + "2_7V": SX126xTcxoCtrl.TCXO_CTRL_2_7V, + "3_0V": SX126xTcxoCtrl.TCXO_CTRL_3_0V, + "3_3V": SX126xTcxoCtrl.TCXO_CTRL_3_3V, + "NONE": SX126xTcxoCtrl.TCXO_CTRL_NONE, +} + +RAMP = { + "10us": SX126xRampTime.PA_RAMP_10, + "20us": SX126xRampTime.PA_RAMP_20, + "40us": SX126xRampTime.PA_RAMP_40, + "80us": SX126xRampTime.PA_RAMP_80, + "200us": SX126xRampTime.PA_RAMP_200, + "800us": SX126xRampTime.PA_RAMP_800, + "1700us": SX126xRampTime.PA_RAMP_1700, + "3400us": SX126xRampTime.PA_RAMP_3400, +} + +SHAPING = { + "GAUSSIAN_BT_0_3": SX126xPulseShape.GAUSSIAN_BT_0_3, + "GAUSSIAN_BT_0_5": SX126xPulseShape.GAUSSIAN_BT_0_5, + "GAUSSIAN_BT_0_7": SX126xPulseShape.GAUSSIAN_BT_0_7, + "GAUSSIAN_BT_1_0": SX126xPulseShape.GAUSSIAN_BT_1_0, + "NONE": SX126xPulseShape.NO_FILTER, +} + +RunImageCalAction = sx126x_ns.class_( + "RunImageCalAction", automation.Action, cg.Parented.template(SX126x) +) +SendPacketAction = sx126x_ns.class_( + "SendPacketAction", automation.Action, cg.Parented.template(SX126x) +) +SetModeTxAction = sx126x_ns.class_( + "SetModeTxAction", automation.Action, cg.Parented.template(SX126x) +) +SetModeRxAction = sx126x_ns.class_( + "SetModeRxAction", automation.Action, cg.Parented.template(SX126x) +) +SetModeSleepAction = sx126x_ns.class_( + "SetModeSleepAction", automation.Action, cg.Parented.template(SX126x) +) +SetModeStandbyAction = sx126x_ns.class_( + "SetModeStandbyAction", automation.Action, cg.Parented.template(SX126x) +) + + +def validate_raw_data(value): + if isinstance(value, str): + return value.encode("utf-8") + if isinstance(value, list): + return cv.Schema([cv.hex_uint8_t])(value) + raise cv.Invalid( + "data must either be a string wrapped in quotes or a list of bytes" + ) + + +def validate_config(config): + lora_bws = [ + "7_8kHz", + "10_4kHz", + "15_6kHz", + "20_8kHz", + "31_3kHz", + "41_7kHz", + "62_5kHz", + "125_0kHz", + "250_0kHz", + "500_0kHz", + ] + if config[CONF_MODULATION] == "LORA": + if config[CONF_BANDWIDTH] not in lora_bws: + raise cv.Invalid(f"{config[CONF_BANDWIDTH]} is not available with LORA") + if config[CONF_PREAMBLE_SIZE] > 0 and config[CONF_PREAMBLE_SIZE] < 6: + raise cv.Invalid("Minimum preamble size is 6 with LORA") + 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") + else: + if config[CONF_BANDWIDTH] in lora_bws: + raise cv.Invalid(f"{config[CONF_BANDWIDTH]} is not available with FSK") + if config[CONF_PREAMBLE_DETECT] > len(config[CONF_SYNC_VALUE]): + raise cv.Invalid("Preamble detection length must be <= sync value length") + return config + + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SX126x), + cv.Optional(CONF_BANDWIDTH, default="125_0kHz"): cv.enum(BW), + cv.Optional(CONF_BITRATE, default=4800): cv.int_range(min=600, max=300000), + cv.Required(CONF_BUSY_PIN): pins.internal_gpio_input_pin_schema, + cv.Optional(CONF_CODING_RATE, default="CR_4_5"): cv.enum(CODING_RATE), + cv.Optional(CONF_CRC_ENABLE, default=False): cv.boolean, + cv.Optional(CONF_DEVIATION, default=5000): cv.int_range(min=0, max=100000), + cv.Required(CONF_DIO1_PIN): pins.internal_gpio_input_pin_schema, + cv.Required(CONF_FREQUENCY): cv.int_range(min=137000000, max=1020000000), + cv.Required(CONF_HW_VERSION): cv.one_of( + "sx1261", "sx1262", "sx1268", "llcc68", lower=True + ), + cv.Required(CONF_MODULATION): cv.enum(MOD), + cv.Optional(CONF_ON_PACKET): automation.validate_automation(single=True), + cv.Optional(CONF_PA_POWER, default=17): cv.int_range(min=-3, max=22), + 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_PREAMBLE_DETECT, default=2): cv.int_range(min=0, max=4), + cv.Required(CONF_PREAMBLE_SIZE): cv.int_range(min=1, max=65535), + cv.Required(CONF_RST_PIN): pins.internal_gpio_output_pin_schema, + cv.Optional(CONF_RX_START, default=True): cv.boolean, + cv.Required(CONF_RF_SWITCH): cv.boolean, + cv.Optional(CONF_SHAPING, default="NONE"): cv.enum(SHAPING), + cv.Optional(CONF_SPREADING_FACTOR, default=7): cv.int_range(min=6, max=12), + cv.Optional(CONF_SYNC_VALUE, default=[]): cv.ensure_list(cv.hex_uint8_t), + cv.Optional(CONF_TCXO_VOLTAGE, default="NONE"): cv.enum(TCXO_VOLTAGE), + cv.Optional(CONF_TCXO_DELAY, default="5ms"): cv.All( + cv.positive_time_period_microseconds, + cv.Range(max=TimePeriod(microseconds=262144000)), + ), + }, + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(spi.spi_device_schema(True, 8e6, "mode0")) + .add_extra(validate_config) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await spi.register_spi_device(var, config) + if CONF_ON_PACKET in config: + await automation.build_automation( + var.get_packet_trigger(), + [ + (cg.std_vector.template(cg.uint8), "x"), + (cg.float_, "rssi"), + (cg.float_, "snr"), + ], + config[CONF_ON_PACKET], + ) + if CONF_DIO1_PIN in config: + dio1_pin = await cg.gpio_pin_expression(config[CONF_DIO1_PIN]) + cg.add(var.set_dio1_pin(dio1_pin)) + rst_pin = await cg.gpio_pin_expression(config[CONF_RST_PIN]) + cg.add(var.set_rst_pin(rst_pin)) + busy_pin = await cg.gpio_pin_expression(config[CONF_BUSY_PIN]) + cg.add(var.set_busy_pin(busy_pin)) + cg.add(var.set_bandwidth(config[CONF_BANDWIDTH])) + cg.add(var.set_frequency(config[CONF_FREQUENCY])) + cg.add(var.set_hw_version(config[CONF_HW_VERSION])) + cg.add(var.set_deviation(config[CONF_DEVIATION])) + cg.add(var.set_modulation(config[CONF_MODULATION])) + cg.add(var.set_pa_ramp(config[CONF_PA_RAMP])) + cg.add(var.set_pa_power(config[CONF_PA_POWER])) + cg.add(var.set_shaping(config[CONF_SHAPING])) + cg.add(var.set_bitrate(config[CONF_BITRATE])) + cg.add(var.set_crc_enable(config[CONF_CRC_ENABLE])) + cg.add(var.set_payload_length(config[CONF_PAYLOAD_LENGTH])) + cg.add(var.set_preamble_size(config[CONF_PREAMBLE_SIZE])) + cg.add(var.set_preamble_detect(config[CONF_PREAMBLE_DETECT])) + cg.add(var.set_coding_rate(config[CONF_CODING_RATE])) + cg.add(var.set_spreading_factor(config[CONF_SPREADING_FACTOR])) + cg.add(var.set_sync_value(config[CONF_SYNC_VALUE])) + cg.add(var.set_rx_start(config[CONF_RX_START])) + cg.add(var.set_rf_switch(config[CONF_RF_SWITCH])) + cg.add(var.set_tcxo_voltage(config[CONF_TCXO_VOLTAGE])) + cg.add(var.set_tcxo_delay(config[CONF_TCXO_DELAY])) + + +NO_ARGS_ACTION_SCHEMA = automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(SX126x), + } +) + + +@automation.register_action( + "sx126x.run_image_cal", RunImageCalAction, NO_ARGS_ACTION_SCHEMA +) +@automation.register_action( + "sx126x.set_mode_tx", SetModeTxAction, NO_ARGS_ACTION_SCHEMA +) +@automation.register_action( + "sx126x.set_mode_rx", SetModeRxAction, NO_ARGS_ACTION_SCHEMA +) +@automation.register_action( + "sx126x.set_mode_sleep", SetModeSleepAction, NO_ARGS_ACTION_SCHEMA +) +@automation.register_action( + "sx126x.set_mode_standby", SetModeStandbyAction, NO_ARGS_ACTION_SCHEMA +) +async def no_args_action_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +SEND_PACKET_ACTION_SCHEMA = cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(SX126x), + cv.Required(CONF_DATA): cv.templatable(validate_raw_data), + }, + key=CONF_DATA, +) + + +@automation.register_action( + "sx126x.send_packet", SendPacketAction, SEND_PACKET_ACTION_SCHEMA +) +async def send_packet_action_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + data = config[CONF_DATA] + if isinstance(data, bytes): + data = list(data) + if cg.is_template(data): + templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8)) + cg.add(var.set_data_template(templ)) + else: + cg.add(var.set_data_static(data)) + return var diff --git a/esphome/components/sx126x/automation.h b/esphome/components/sx126x/automation.h new file mode 100644 index 0000000000..520ef99718 --- /dev/null +++ b/esphome/components/sx126x/automation.h @@ -0,0 +1,62 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/sx126x/sx126x.h" + +namespace esphome { +namespace sx126x { + +template class RunImageCalAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->run_image_cal(); } +}; + +template class SendPacketAction : public Action, public Parented { + public: + void set_data_template(std::function(Ts...)> func) { + this->data_func_ = func; + this->static_ = false; + } + + void set_data_static(const std::vector &data) { + this->data_static_ = data; + this->static_ = true; + } + + void play(Ts... x) override { + if (this->static_) { + this->parent_->transmit_packet(this->data_static_); + } else { + this->parent_->transmit_packet(this->data_func_(x...)); + } + } + + protected: + bool static_{false}; + std::function(Ts...)> data_func_{}; + std::vector data_static_{}; +}; + +template class SetModeTxAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->set_mode_tx(); } +}; + +template class SetModeRxAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->set_mode_rx(); } +}; + +template class SetModeSleepAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->set_mode_sleep(); } +}; + +template class SetModeStandbyAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->set_mode_standby(STDBY_XOSC); } +}; + +} // namespace sx126x +} // namespace esphome diff --git a/esphome/components/sx126x/packet_transport/__init__.py b/esphome/components/sx126x/packet_transport/__init__.py new file mode 100644 index 0000000000..4d79b23ac1 --- /dev/null +++ b/esphome/components/sx126x/packet_transport/__init__.py @@ -0,0 +1,26 @@ +import esphome.codegen as cg +from esphome.components.packet_transport import ( + PacketTransport, + new_packet_transport, + transport_schema, +) +import esphome.config_validation as cv +from esphome.cpp_types import PollingComponent + +from .. import CONF_SX126X_ID, SX126x, SX126xListener, sx126x_ns + +SX126xTransport = sx126x_ns.class_( + "SX126xTransport", PacketTransport, PollingComponent, SX126xListener +) + +CONFIG_SCHEMA = transport_schema(SX126xTransport).extend( + { + cv.GenerateID(CONF_SX126X_ID): cv.use_id(SX126x), + } +) + + +async def to_code(config): + var, _ = await new_packet_transport(config) + sx126x = await cg.get_variable(config[CONF_SX126X_ID]) + cg.add(var.set_parent(sx126x)) diff --git a/esphome/components/sx126x/packet_transport/sx126x_transport.cpp b/esphome/components/sx126x/packet_transport/sx126x_transport.cpp new file mode 100644 index 0000000000..2cfc4b700e --- /dev/null +++ b/esphome/components/sx126x/packet_transport/sx126x_transport.cpp @@ -0,0 +1,26 @@ +#include "esphome/core/log.h" +#include "esphome/core/application.h" +#include "sx126x_transport.h" + +namespace esphome { +namespace sx126x { + +static const char *const TAG = "sx126x_transport"; + +void SX126xTransport::setup() { + PacketTransport::setup(); + this->parent_->register_listener(this); +} + +void SX126xTransport::update() { + PacketTransport::update(); + this->updated_ = true; + this->resend_data_ = true; +} + +void SX126xTransport::send_packet(const std::vector &buf) const { this->parent_->transmit_packet(buf); } + +void SX126xTransport::on_packet(const std::vector &packet, float rssi, float snr) { this->process_(packet); } + +} // namespace sx126x +} // namespace esphome diff --git a/esphome/components/sx126x/packet_transport/sx126x_transport.h b/esphome/components/sx126x/packet_transport/sx126x_transport.h new file mode 100644 index 0000000000..755d30417d --- /dev/null +++ b/esphome/components/sx126x/packet_transport/sx126x_transport.h @@ -0,0 +1,25 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sx126x/sx126x.h" +#include "esphome/components/packet_transport/packet_transport.h" +#include + +namespace esphome { +namespace sx126x { + +class SX126xTransport : public packet_transport::PacketTransport, public Parented, public SX126xListener { + public: + void setup() override; + void update() override; + void on_packet(const std::vector &packet, float rssi, float snr) override; + float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + + protected: + void send_packet(const std::vector &buf) const override; + bool should_send() override { return true; } + size_t get_max_packet_size() override { return this->parent_->get_max_packet_size(); } +}; + +} // namespace sx126x +} // namespace esphome diff --git a/esphome/components/sx126x/sx126x.cpp b/esphome/components/sx126x/sx126x.cpp new file mode 100644 index 0000000000..b1c81b324a --- /dev/null +++ b/esphome/components/sx126x/sx126x.cpp @@ -0,0 +1,523 @@ +#include "sx126x.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace sx126x { + +static const char *const TAG = "sx126x"; +static const uint16_t RAMP[8] = {10, 20, 40, 80, 200, 800, 1700, 3400}; +static const uint32_t BW_HZ[31] = {4800, 5800, 7300, 9700, 11700, 14600, 19500, 23400, 29300, 39000, 46900, + 58600, 78200, 93800, 117300, 156200, 187200, 234300, 312000, 373600, 467000, 7810, + 10420, 15630, 20830, 31250, 41670, 62500, 125000, 250000, 500000}; +static const uint8_t BW_LORA[10] = {LORA_BW_7810, LORA_BW_10420, LORA_BW_15630, LORA_BW_20830, LORA_BW_31250, + LORA_BW_41670, LORA_BW_62500, LORA_BW_125000, LORA_BW_250000, LORA_BW_500000}; +static const uint8_t BW_FSK[21] = { + FSK_BW_4800, FSK_BW_5800, FSK_BW_7300, FSK_BW_9700, FSK_BW_11700, FSK_BW_14600, FSK_BW_19500, + FSK_BW_23400, FSK_BW_29300, FSK_BW_39000, FSK_BW_46900, FSK_BW_58600, FSK_BW_78200, FSK_BW_93800, + FSK_BW_117300, FSK_BW_156200, FSK_BW_187200, FSK_BW_234300, FSK_BW_312000, FSK_BW_373600, FSK_BW_467000}; + +static constexpr uint32_t RESET_DELAY_HIGH_US = 5000; +static constexpr uint32_t RESET_DELAY_LOW_US = 2000; +static constexpr uint32_t SWITCHING_DELAY_US = 1; +static constexpr uint32_t TRANSMIT_TIMEOUT_MS = 4000; +static constexpr uint32_t BUSY_TIMEOUT_MS = 20; + +// OCP (Over Current Protection) values +static constexpr uint8_t OCP_80MA = 0x18; // 80 mA max current +static constexpr uint8_t OCP_140MA = 0x38; // 140 mA max current + +// LoRa low data rate optimization threshold +static constexpr float LOW_DATA_RATE_OPTIMIZE_THRESHOLD = 16.38f; // 16.38 ms + +uint8_t SX126x::read_fifo_(uint8_t offset, std::vector &packet) { + this->wait_busy_(); + this->enable(); + this->transfer_byte(RADIO_READ_BUFFER); + this->transfer_byte(offset); + uint8_t status = this->transfer_byte(0x00); + for (uint8_t &byte : packet) { + byte = this->transfer_byte(0x00); + } + this->disable(); + return status; +} + +void SX126x::write_fifo_(uint8_t offset, const std::vector &packet) { + this->wait_busy_(); + this->enable(); + this->transfer_byte(RADIO_WRITE_BUFFER); + this->transfer_byte(offset); + for (const uint8_t &byte : packet) { + this->transfer_byte(byte); + } + this->disable(); + delayMicroseconds(SWITCHING_DELAY_US); +} + +uint8_t SX126x::read_opcode_(uint8_t opcode, uint8_t *data, uint8_t size) { + this->wait_busy_(); + this->enable(); + this->transfer_byte(opcode); + uint8_t status = this->transfer_byte(0x00); + for (int32_t i = 0; i < size; i++) { + data[i] = this->transfer_byte(0x00); + } + this->disable(); + return status; +} + +void SX126x::write_opcode_(uint8_t opcode, uint8_t *data, uint8_t size) { + this->wait_busy_(); + this->enable(); + this->transfer_byte(opcode); + for (int32_t i = 0; i < size; i++) { + this->transfer_byte(data[i]); + } + this->disable(); + delayMicroseconds(SWITCHING_DELAY_US); +} + +void SX126x::read_register_(uint16_t reg, uint8_t *data, uint8_t size) { + this->wait_busy_(); + this->enable(); + this->write_byte(RADIO_READ_REGISTER); + this->write_byte((reg >> 8) & 0xFF); + this->write_byte((reg >> 0) & 0xFF); + this->write_byte(0x00); + for (int32_t i = 0; i < size; i++) { + data[i] = this->transfer_byte(0x00); + } + this->disable(); +} + +void SX126x::write_register_(uint16_t reg, uint8_t *data, uint8_t size) { + this->wait_busy_(); + this->enable(); + this->write_byte(RADIO_WRITE_REGISTER); + this->write_byte((reg >> 8) & 0xFF); + this->write_byte((reg >> 0) & 0xFF); + for (int32_t i = 0; i < size; i++) { + this->transfer_byte(data[i]); + } + this->disable(); + delayMicroseconds(SWITCHING_DELAY_US); +} + +void SX126x::setup() { + ESP_LOGCONFIG(TAG, "Running setup"); + + // setup pins + this->busy_pin_->setup(); + this->rst_pin_->setup(); + this->dio1_pin_->setup(); + + // start spi + this->spi_setup(); + + // configure rf + this->configure(); +} + +void SX126x::configure() { + uint8_t buf[8]; + + // toggle chip reset + this->rst_pin_->digital_write(true); + delayMicroseconds(RESET_DELAY_HIGH_US); + this->rst_pin_->digital_write(false); + delayMicroseconds(RESET_DELAY_LOW_US); + this->rst_pin_->digital_write(true); + delayMicroseconds(RESET_DELAY_HIGH_US); + + // wakeup + this->read_opcode_(RADIO_GET_STATUS, nullptr, 0); + + // config tcxo + if (this->tcxo_voltage_ != TCXO_CTRL_NONE) { + uint32_t delay = this->tcxo_delay_ >> 6; + buf[0] = this->tcxo_voltage_; + buf[1] = (delay >> 16) & 0xFF; + buf[2] = (delay >> 8) & 0xFF; + buf[3] = (delay >> 0) & 0xFF; + this->write_opcode_(RADIO_SET_TCXOMODE, buf, 4); + buf[0] = 0x7F; + this->write_opcode_(RADIO_CALIBRATE, buf, 1); + } + + // clear errors + buf[0] = 0x00; + buf[1] = 0x00; + this->write_opcode_(RADIO_CLR_ERROR, buf, 2); + + // rf switch + if (this->rf_switch_) { + buf[0] = 0x01; + this->write_opcode_(RADIO_SET_RFSWITCHMODE, buf, 1); + } + + // check silicon version to make sure hw is ok + this->read_register_(REG_VERSION_STRING, (uint8_t *) this->version_, 16); + if (strncmp(this->version_, "SX126", 5) != 0 && strncmp(this->version_, "LLCC68", 6) != 0) { + this->mark_failed(); + return; + } + + // setup packet type + buf[0] = this->modulation_; + this->write_opcode_(RADIO_SET_PACKETTYPE, buf, 1); + + // calibrate image + this->run_image_cal(); + + // set frequency + uint64_t freq = ((uint64_t) this->frequency_ << 25) / XTAL_FREQ; + buf[0] = (uint8_t) ((freq >> 24) & 0xFF); + buf[1] = (uint8_t) ((freq >> 16) & 0xFF); + buf[2] = (uint8_t) ((freq >> 8) & 0xFF); + buf[3] = (uint8_t) (freq & 0xFF); + this->write_opcode_(RADIO_SET_RFFREQUENCY, buf, 4); + + // configure pa + int8_t pa_power = this->pa_power_; + if (this->hw_version_ == "sx1261") { + // the following values were taken from section 13.1.14.1 table 13-21 + // in rev 2.1 of the datasheet + if (pa_power == 15) { + uint8_t cfg[4] = {0x06, 0x00, 0x01, 0x01}; + this->write_opcode_(RADIO_SET_PACONFIG, cfg, 4); + } else { + uint8_t cfg[4] = {0x04, 0x00, 0x01, 0x01}; + this->write_opcode_(RADIO_SET_PACONFIG, cfg, 4); + } + pa_power = std::max(pa_power, (int8_t) -3); + pa_power = std::min(pa_power, (int8_t) 14); + buf[0] = OCP_80MA; + this->write_register_(REG_OCP, buf, 1); + } else { + // the following values were taken from section 13.1.14.1 table 13-21 + // in rev 2.1 of the datasheet + uint8_t cfg[4] = {0x04, 0x07, 0x00, 0x01}; + this->write_opcode_(RADIO_SET_PACONFIG, cfg, 4); + pa_power = std::max(pa_power, (int8_t) -3); + pa_power = std::min(pa_power, (int8_t) 22); + buf[0] = OCP_140MA; + this->write_register_(REG_OCP, buf, 1); + } + buf[0] = pa_power; + buf[1] = this->pa_ramp_; + this->write_opcode_(RADIO_SET_TXPARAMS, buf, 2); + + // configure modem + if (this->modulation_ == PACKET_TYPE_LORA) { + // set modulation params + float duration = 1000.0f * std::pow(2, this->spreading_factor_) / BW_HZ[this->bandwidth_]; + buf[0] = this->spreading_factor_; + buf[1] = BW_LORA[this->bandwidth_ - SX126X_BW_7810]; + buf[2] = this->coding_rate_; + buf[3] = (duration > LOW_DATA_RATE_OPTIMIZE_THRESHOLD) ? 0x01 : 0x00; + this->write_opcode_(RADIO_SET_MODULATIONPARAMS, buf, 4); + + // set packet params and sync word + this->set_packet_params_(this->payload_length_); + if (this->sync_value_.size() == 2) { + this->write_register_(REG_LORA_SYNCWORD, this->sync_value_.data(), this->sync_value_.size()); + } + } else { + // set modulation params + uint32_t bitrate = ((uint64_t) XTAL_FREQ * 32) / this->bitrate_; + uint32_t fdev = ((uint64_t) this->deviation_ << 25) / XTAL_FREQ; + buf[0] = (bitrate >> 16) & 0xFF; + buf[1] = (bitrate >> 8) & 0xFF; + buf[2] = (bitrate >> 0) & 0xFF; + buf[3] = this->shaping_; + buf[4] = BW_FSK[this->bandwidth_ - SX126X_BW_4800]; + buf[5] = (fdev >> 16) & 0xFF; + buf[6] = (fdev >> 8) & 0xFF; + buf[7] = (fdev >> 0) & 0xFF; + this->write_opcode_(RADIO_SET_MODULATIONPARAMS, buf, 8); + + // set packet params and sync word + this->set_packet_params_(this->payload_length_); + if (!this->sync_value_.empty()) { + this->write_register_(REG_GFSK_SYNCWORD, this->sync_value_.data(), this->sync_value_.size()); + } + } + + // switch to rx or sleep + if (this->rx_start_) { + this->set_mode_rx(); + } else { + this->set_mode_sleep(); + } +} + +size_t SX126x::get_max_packet_size() { + if (this->payload_length_ > 0) { + return this->payload_length_; + } + return 255; +} + +void SX126x::set_packet_params_(uint8_t payload_length) { + uint8_t buf[9]; + if (this->modulation_ == PACKET_TYPE_LORA) { + buf[0] = (this->preamble_size_ >> 8) & 0xFF; + buf[1] = (this->preamble_size_ >> 0) & 0xFF; + buf[2] = (this->payload_length_ > 0) ? 0x01 : 0x00; + buf[3] = payload_length; + buf[4] = (this->crc_enable_) ? 0x01 : 0x00; + buf[5] = 0x00; + this->write_opcode_(RADIO_SET_PACKETPARAMS, buf, 6); + } else { + uint16_t preamble_size = this->preamble_size_ * 8; + buf[0] = (preamble_size >> 8) & 0xFF; + buf[1] = (preamble_size >> 0) & 0xFF; + buf[2] = (this->preamble_detect_ > 0) ? ((this->preamble_detect_ - 1) | 0x04) : 0x00; + buf[3] = this->sync_value_.size() * 8; + buf[4] = 0x00; + buf[5] = 0x00; + buf[6] = payload_length; + buf[7] = this->crc_enable_ ? 0x06 : 0x01; + buf[8] = 0x00; + this->write_opcode_(RADIO_SET_PACKETPARAMS, buf, 9); + } +} + +SX126xError SX126x::transmit_packet(const std::vector &packet) { + if (this->payload_length_ > 0 && this->payload_length_ != packet.size()) { + ESP_LOGE(TAG, "Packet size does not match config"); + return SX126xError::INVALID_PARAMS; + } + if (packet.empty() || packet.size() > this->get_max_packet_size()) { + ESP_LOGE(TAG, "Packet size out of range"); + return SX126xError::INVALID_PARAMS; + } + + SX126xError ret = SX126xError::NONE; + this->set_mode_standby(STDBY_XOSC); + if (this->payload_length_ == 0) { + this->set_packet_params_(packet.size()); + } + this->write_fifo_(0x00, packet); + this->set_mode_tx(); + + // wait until transmit completes, typically the delay will be less than 100 ms + uint32_t start = millis(); + while (!this->dio1_pin_->digital_read()) { + if (millis() - start > TRANSMIT_TIMEOUT_MS) { + ESP_LOGE(TAG, "Transmit packet failure"); + ret = SX126xError::TIMEOUT; + break; + } + } + + uint8_t buf[2]; + buf[0] = 0xFF; + buf[1] = 0xFF; + this->write_opcode_(RADIO_CLR_IRQSTATUS, buf, 2); + if (this->rx_start_) { + this->set_mode_rx(); + } else { + this->set_mode_sleep(); + } + return ret; +} + +void SX126x::call_listeners_(const std::vector &packet, float rssi, float snr) { + for (auto &listener : this->listeners_) { + listener->on_packet(packet, rssi, snr); + } + this->packet_trigger_->trigger(packet, rssi, snr); +} + +void SX126x::loop() { + if (!this->dio1_pin_->digital_read()) { + return; + } + + uint16_t status; + uint8_t buf[3]; + uint8_t rssi; + int8_t snr; + this->read_opcode_(RADIO_GET_IRQSTATUS, buf, 2); + this->write_opcode_(RADIO_CLR_IRQSTATUS, buf, 2); + status = (buf[0] << 8) | buf[1]; + if ((status & IRQ_RX_DONE) == IRQ_RX_DONE) { + if ((status & IRQ_CRC_ERROR) != IRQ_CRC_ERROR) { + this->read_opcode_(RADIO_GET_PACKETSTATUS, buf, 3); + if (this->modulation_ == PACKET_TYPE_LORA) { + rssi = buf[0]; + snr = buf[1]; + } else { + rssi = buf[2]; + snr = 0; + } + this->read_opcode_(RADIO_GET_RXBUFFERSTATUS, buf, 2); + this->packet_.resize(buf[0]); + this->read_fifo_(buf[1], this->packet_); + this->call_listeners_(this->packet_, (float) rssi / -2.0f, (float) snr / 4.0f); + } + } +} + +void SX126x::run_image_cal() { + // the following values were taken from section 9.2.1 table 9-2 + // in rev 2.1 of the datasheet + uint8_t buf[2] = {0, 0}; + if (this->frequency_ > 900000000) { + buf[0] = 0xE1; + buf[1] = 0xE9; + } else if (this->frequency_ > 850000000) { + buf[0] = 0xD7; + buf[1] = 0xD8; + } else if (this->frequency_ > 770000000) { + buf[0] = 0xC1; + buf[1] = 0xC5; + } else if (this->frequency_ > 460000000) { + buf[0] = 0x75; + buf[1] = 0x81; + } else if (this->frequency_ > 425000000) { + buf[0] = 0x6B; + buf[1] = 0x6F; + } + if (buf[0] > 0 && buf[1] > 0) { + this->write_opcode_(RADIO_CALIBRATEIMAGE, buf, 2); + } +} + +void SX126x::set_mode_rx() { + uint8_t buf[8]; + + // configure irq params + uint16_t irq = IRQ_RX_DONE | IRQ_RX_TX_TIMEOUT | IRQ_CRC_ERROR; + buf[0] = (irq >> 8) & 0xFF; + buf[1] = (irq >> 0) & 0xFF; + buf[2] = (irq >> 8) & 0xFF; + buf[3] = (irq >> 0) & 0xFF; + buf[4] = (IRQ_RADIO_NONE >> 8) & 0xFF; + buf[5] = (IRQ_RADIO_NONE >> 0) & 0xFF; + buf[6] = (IRQ_RADIO_NONE >> 8) & 0xFF; + buf[7] = (IRQ_RADIO_NONE >> 0) & 0xFF; + this->write_opcode_(RADIO_SET_DIOIRQPARAMS, buf, 8); + + // set timeout to 0 + buf[0] = 0x00; + this->write_opcode_(RADIO_SET_LORASYMBTIMEOUT, buf, 1); + + // switch to continuous mode rx + buf[0] = 0xFF; + buf[1] = 0xFF; + buf[2] = 0xFF; + this->write_opcode_(RADIO_SET_RX, buf, 3); +} + +void SX126x::set_mode_tx() { + uint8_t buf[8]; + + // configure irq params + uint16_t irq = IRQ_TX_DONE | IRQ_RX_TX_TIMEOUT; + buf[0] = (irq >> 8) & 0xFF; + buf[1] = (irq >> 0) & 0xFF; + buf[2] = (irq >> 8) & 0xFF; + buf[3] = (irq >> 0) & 0xFF; + buf[4] = (IRQ_RADIO_NONE >> 8) & 0xFF; + buf[5] = (IRQ_RADIO_NONE >> 0) & 0xFF; + buf[6] = (IRQ_RADIO_NONE >> 8) & 0xFF; + buf[7] = (IRQ_RADIO_NONE >> 0) & 0xFF; + this->write_opcode_(RADIO_SET_DIOIRQPARAMS, buf, 8); + + // switch to single mode tx + buf[0] = 0x00; + buf[1] = 0x00; + buf[2] = 0x00; + this->write_opcode_(RADIO_SET_TX, buf, 3); +} + +void SX126x::set_mode_sleep() { + uint8_t buf[1]; + buf[0] = 0x05; + this->write_opcode_(RADIO_SET_SLEEP, buf, 1); +} + +void SX126x::set_mode_standby(SX126xStandbyMode mode) { + uint8_t buf[1]; + buf[0] = mode; + this->write_opcode_(RADIO_SET_STANDBY, buf, 1); +} + +void SX126x::wait_busy_() { + // wait if the device is busy, the maximum delay is only be a few ms + // with most commands taking only a few us + uint32_t start = millis(); + while (this->busy_pin_->digital_read()) { + if (millis() - start > BUSY_TIMEOUT_MS) { + ESP_LOGE(TAG, "Wait busy timeout"); + this->mark_failed(); + break; + } + } +} + +void SX126x::dump_config() { + ESP_LOGCONFIG(TAG, "SX126x:"); + LOG_PIN(" CS Pin: ", this->cs_); + LOG_PIN(" BUSY Pin: ", this->busy_pin_); + LOG_PIN(" RST Pin: ", this->rst_pin_); + LOG_PIN(" DIO1 Pin: ", this->dio1_pin_); + ESP_LOGCONFIG(TAG, + " HW Version: %15s\n" + " Frequency: %" PRIu32 " Hz\n" + " Bandwidth: %" PRIu32 " Hz\n" + " PA Power: %" PRId8 " dBm\n" + " PA Ramp: %" PRIu16 " us\n" + " Payload Length: %" PRIu32 "\n" + " CRC Enable: %s\n" + " Rx Start: %s", + this->version_, this->frequency_, BW_HZ[this->bandwidth_], this->pa_power_, RAMP[this->pa_ramp_], + this->payload_length_, TRUEFALSE(this->crc_enable_), TRUEFALSE(this->rx_start_)); + if (this->modulation_ == PACKET_TYPE_GFSK) { + const char *shaping = "NONE"; + if (this->shaping_ == GAUSSIAN_BT_0_3) { + shaping = "GAUSSIAN_BT_0_3"; + } else if (this->shaping_ == GAUSSIAN_BT_0_5) { + shaping = "GAUSSIAN_BT_0_5"; + } else if (this->shaping_ == GAUSSIAN_BT_0_7) { + shaping = "GAUSSIAN_BT_0_7"; + } else if (this->shaping_ == GAUSSIAN_BT_1_0) { + shaping = "GAUSSIAN_BT_1_0"; + } + ESP_LOGCONFIG(TAG, + " Modulation: FSK\n" + " Deviation: %" PRIu32 " Hz\n" + " Shaping: %s\n" + " Preamble Size: %" PRIu16 "\n" + " Preamble Detect: %" PRIu16 "\n" + " Bitrate: %" PRIu32 "b/s", + this->deviation_, shaping, this->preamble_size_, this->preamble_detect_, this->bitrate_); + } else if (this->modulation_ == PACKET_TYPE_LORA) { + const char *cr = "4/8"; + if (this->coding_rate_ == LORA_CR_4_5) { + cr = "4/5"; + } else if (this->coding_rate_ == LORA_CR_4_6) { + cr = "4/6"; + } else if (this->coding_rate_ == LORA_CR_4_7) { + cr = "4/7"; + } + ESP_LOGCONFIG(TAG, + " Modulation: LORA\n" + " Spreading Factor: %" PRIu8 "\n" + " Coding Rate: %s\n" + " Preamble Size: %" PRIu16, + this->spreading_factor_, cr, this->preamble_size_); + } + if (!this->sync_value_.empty()) { + ESP_LOGCONFIG(TAG, " Sync Value: 0x%s", format_hex(this->sync_value_).c_str()); + } + if (this->is_failed()) { + ESP_LOGE(TAG, "Configuring SX126x failed"); + } +} + +} // namespace sx126x +} // namespace esphome diff --git a/esphome/components/sx126x/sx126x.h b/esphome/components/sx126x/sx126x.h new file mode 100644 index 0000000000..fd5c37942d --- /dev/null +++ b/esphome/components/sx126x/sx126x.h @@ -0,0 +1,140 @@ +#pragma once + +#include "esphome/components/spi/spi.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "sx126x_reg.h" +#include +#include + +namespace esphome { +namespace sx126x { + +enum SX126xBw : uint8_t { + // FSK + SX126X_BW_4800, + SX126X_BW_5800, + SX126X_BW_7300, + SX126X_BW_9700, + SX126X_BW_11700, + SX126X_BW_14600, + SX126X_BW_19500, + SX126X_BW_23400, + SX126X_BW_29300, + SX126X_BW_39000, + SX126X_BW_46900, + SX126X_BW_58600, + SX126X_BW_78200, + SX126X_BW_93800, + SX126X_BW_117300, + SX126X_BW_156200, + SX126X_BW_187200, + SX126X_BW_234300, + SX126X_BW_312000, + SX126X_BW_373600, + SX126X_BW_467000, + // LORA + SX126X_BW_7810, + SX126X_BW_10420, + SX126X_BW_15630, + SX126X_BW_20830, + SX126X_BW_31250, + SX126X_BW_41670, + SX126X_BW_62500, + SX126X_BW_125000, + SX126X_BW_250000, + SX126X_BW_500000, +}; + +enum class SX126xError { NONE = 0, TIMEOUT, INVALID_PARAMS }; + +class SX126xListener { + public: + virtual void on_packet(const std::vector &packet, float rssi, float snr) = 0; +}; + +class SX126x : public Component, + public spi::SPIDevice { + public: + size_t get_max_packet_size(); + float get_setup_priority() const override { return setup_priority::PROCESSOR; } + void setup() override; + void loop() override; + void dump_config() override; + void set_bandwidth(SX126xBw bandwidth) { this->bandwidth_ = bandwidth; } + void set_bitrate(uint32_t bitrate) { this->bitrate_ = bitrate; } + void set_busy_pin(InternalGPIOPin *busy_pin) { this->busy_pin_ = busy_pin; } + void set_coding_rate(uint8_t coding_rate) { this->coding_rate_ = coding_rate; } + void set_crc_enable(bool crc_enable) { this->crc_enable_ = crc_enable; } + void set_deviation(uint32_t deviation) { this->deviation_ = deviation; } + void set_dio1_pin(InternalGPIOPin *dio1_pin) { this->dio1_pin_ = dio1_pin; } + void set_frequency(uint32_t frequency) { this->frequency_ = frequency; } + void set_hw_version(const std::string &hw_version) { this->hw_version_ = hw_version; } + void set_mode_rx(); + void set_mode_tx(); + void set_mode_standby(SX126xStandbyMode mode); + void set_mode_sleep(); + void set_modulation(uint8_t modulation) { this->modulation_ = modulation; } + void set_pa_power(int8_t power) { this->pa_power_ = power; } + void set_pa_ramp(uint8_t ramp) { this->pa_ramp_ = ramp; } + void set_payload_length(uint8_t payload_length) { this->payload_length_ = payload_length; } + void set_preamble_detect(uint16_t preamble_detect) { this->preamble_detect_ = preamble_detect; } + void set_preamble_size(uint16_t preamble_size) { this->preamble_size_ = preamble_size; } + void set_rst_pin(InternalGPIOPin *rst_pin) { this->rst_pin_ = rst_pin; } + void set_rx_start(bool rx_start) { this->rx_start_ = rx_start; } + void set_rf_switch(bool rf_switch) { this->rf_switch_ = rf_switch; } + void set_shaping(uint8_t shaping) { this->shaping_ = shaping; } + void set_spreading_factor(uint8_t spreading_factor) { this->spreading_factor_ = spreading_factor; } + void set_sync_value(const std::vector &sync_value) { this->sync_value_ = sync_value; } + void set_tcxo_voltage(uint8_t tcxo_voltage) { this->tcxo_voltage_ = tcxo_voltage; } + void set_tcxo_delay(uint32_t tcxo_delay) { this->tcxo_delay_ = tcxo_delay; } + void run_image_cal(); + void configure(); + SX126xError transmit_packet(const std::vector &packet); + void register_listener(SX126xListener *listener) { this->listeners_.push_back(listener); } + Trigger, float, float> *get_packet_trigger() const { return this->packet_trigger_; }; + + protected: + void configure_fsk_ook_(); + void configure_lora_(); + void set_packet_params_(uint8_t payload_length); + uint8_t read_fifo_(uint8_t offset, std::vector &packet); + void write_fifo_(uint8_t offset, const std::vector &packet); + void write_opcode_(uint8_t opcode, uint8_t *data, uint8_t size); + uint8_t read_opcode_(uint8_t opcode, uint8_t *data, uint8_t size); + void write_register_(uint16_t reg, uint8_t *data, uint8_t size); + void read_register_(uint16_t reg, uint8_t *data, uint8_t size); + void call_listeners_(const std::vector &packet, float rssi, float snr); + void wait_busy_(); + Trigger, float, float> *packet_trigger_{new Trigger, float, float>()}; + std::vector listeners_; + std::vector packet_; + std::vector sync_value_; + InternalGPIOPin *busy_pin_{nullptr}; + InternalGPIOPin *dio1_pin_{nullptr}; + InternalGPIOPin *rst_pin_{nullptr}; + std::string hw_version_; + char version_[16]; + SX126xBw bandwidth_{SX126X_BW_125000}; + uint32_t bitrate_{0}; + uint32_t deviation_{0}; + uint32_t frequency_{0}; + uint32_t payload_length_{0}; + uint32_t tcxo_delay_{0}; + uint16_t preamble_detect_{0}; + uint16_t preamble_size_{0}; + uint8_t tcxo_voltage_{0}; + uint8_t coding_rate_{0}; + uint8_t modulation_{PACKET_TYPE_LORA}; + uint8_t pa_ramp_{0}; + uint8_t shaping_{0}; + uint8_t spreading_factor_{0}; + int8_t pa_power_{0}; + bool crc_enable_{false}; + bool rx_start_{false}; + bool rf_switch_{false}; +}; + +} // namespace sx126x +} // namespace esphome diff --git a/esphome/components/sx126x/sx126x_reg.h b/esphome/components/sx126x/sx126x_reg.h new file mode 100644 index 0000000000..3b12d822b5 --- /dev/null +++ b/esphome/components/sx126x/sx126x_reg.h @@ -0,0 +1,163 @@ +#pragma once + +#include "esphome/core/hal.h" + +namespace esphome { +namespace sx126x { + +static const uint32_t XTAL_FREQ = 32000000; + +enum SX126xOpCode : uint8_t { + RADIO_GET_STATUS = 0xC0, + RADIO_WRITE_REGISTER = 0x0D, + RADIO_READ_REGISTER = 0x1D, + RADIO_WRITE_BUFFER = 0x0E, + RADIO_READ_BUFFER = 0x1E, + RADIO_SET_SLEEP = 0x84, + RADIO_SET_STANDBY = 0x80, + RADIO_SET_FS = 0xC1, + RADIO_SET_TX = 0x83, + RADIO_SET_RX = 0x82, + RADIO_SET_RXDUTYCYCLE = 0x94, + RADIO_SET_CAD = 0xC5, + RADIO_SET_TXCONTINUOUSWAVE = 0xD1, + RADIO_SET_TXCONTINUOUSPREAMBLE = 0xD2, + RADIO_SET_PACKETTYPE = 0x8A, + RADIO_GET_PACKETTYPE = 0x11, + RADIO_SET_RFFREQUENCY = 0x86, + RADIO_SET_TXPARAMS = 0x8E, + RADIO_SET_PACONFIG = 0x95, + RADIO_SET_CADPARAMS = 0x88, + RADIO_SET_BUFFERBASEADDRESS = 0x8F, + RADIO_SET_MODULATIONPARAMS = 0x8B, + RADIO_SET_PACKETPARAMS = 0x8C, + RADIO_GET_RXBUFFERSTATUS = 0x13, + RADIO_GET_PACKETSTATUS = 0x14, + RADIO_GET_RSSIINST = 0x15, + RADIO_GET_STATS = 0x10, + RADIO_RESET_STATS = 0x00, + RADIO_SET_DIOIRQPARAMS = 0x08, + RADIO_GET_IRQSTATUS = 0x12, + RADIO_CLR_IRQSTATUS = 0x02, + RADIO_CALIBRATE = 0x89, + RADIO_CALIBRATEIMAGE = 0x98, + RADIO_SET_REGULATORMODE = 0x96, + RADIO_GET_ERROR = 0x17, + RADIO_CLR_ERROR = 0x07, + RADIO_SET_TCXOMODE = 0x97, + RADIO_SET_TXFALLBACKMODE = 0x93, + RADIO_SET_RFSWITCHMODE = 0x9D, + RADIO_SET_STOPRXTIMERONPREAMBLE = 0x9F, + RADIO_SET_LORASYMBTIMEOUT = 0xA0, +}; + +enum SX126xRegister : uint16_t { + REG_VERSION_STRING = 0x0320, + REG_GFSK_SYNCWORD = 0x06C0, + REG_LORA_SYNCWORD = 0x0740, + REG_OCP = 0x08E7, +}; + +enum SX126xStandbyMode : uint8_t { + STDBY_RC = 0x00, + STDBY_XOSC = 0x01, +}; + +enum SX126xPacketType : uint8_t { + PACKET_TYPE_GFSK = 0x00, + PACKET_TYPE_LORA = 0x01, + PACKET_TYPE_LRHSS = 0x03, +}; + +enum SX126xFskBw : uint8_t { + FSK_BW_4800 = 0x1F, + FSK_BW_5800 = 0x17, + FSK_BW_7300 = 0x0F, + FSK_BW_9700 = 0x1E, + FSK_BW_11700 = 0x16, + FSK_BW_14600 = 0x0E, + FSK_BW_19500 = 0x1D, + FSK_BW_23400 = 0x15, + FSK_BW_29300 = 0x0D, + FSK_BW_39000 = 0x1C, + FSK_BW_46900 = 0x14, + FSK_BW_58600 = 0x0C, + FSK_BW_78200 = 0x1B, + FSK_BW_93800 = 0x13, + FSK_BW_117300 = 0x0B, + FSK_BW_156200 = 0x1A, + FSK_BW_187200 = 0x12, + FSK_BW_234300 = 0x0A, + FSK_BW_312000 = 0x19, + FSK_BW_373600 = 0x11, + FSK_BW_467000 = 0x09, +}; + +enum SX126xLoraBw : uint8_t { + LORA_BW_7810 = 0x00, + LORA_BW_10420 = 0x08, + LORA_BW_15630 = 0x01, + LORA_BW_20830 = 0x09, + LORA_BW_31250 = 0x02, + LORA_BW_41670 = 0x0A, + LORA_BW_62500 = 0x03, + LORA_BW_125000 = 0x04, + LORA_BW_250000 = 0x05, + LORA_BW_500000 = 0x06, +}; + +enum SX126xLoraCr : uint8_t { + LORA_CR_4_5 = 0x01, + LORA_CR_4_6 = 0x02, + LORA_CR_4_7 = 0x03, + LORA_CR_4_8 = 0x04, +}; + +enum SX126xIrqMasks : uint16_t { + IRQ_RADIO_NONE = 0x0000, + IRQ_TX_DONE = 0x0001, + IRQ_RX_DONE = 0x0002, + IRQ_PREAMBLE_DETECTED = 0x0004, + IRQ_SYNCWORD_VALID = 0x0008, + IRQ_HEADER_VALID = 0x0010, + IRQ_HEADER_ERROR = 0x0020, + IRQ_CRC_ERROR = 0x0040, + IRQ_CAD_DONE = 0x0080, + IRQ_CAD_ACTIVITY_DETECTED = 0x0100, + IRQ_RX_TX_TIMEOUT = 0x0200, + IRQ_RADIO_ALL = 0xFFFF, +}; + +enum SX126xTcxoCtrl : uint8_t { + TCXO_CTRL_1_6V = 0x00, + TCXO_CTRL_1_7V = 0x01, + TCXO_CTRL_1_8V = 0x02, + TCXO_CTRL_2_2V = 0x03, + TCXO_CTRL_2_4V = 0x04, + TCXO_CTRL_2_7V = 0x05, + TCXO_CTRL_3_0V = 0x06, + TCXO_CTRL_3_3V = 0x07, + TCXO_CTRL_NONE = 0xFF, +}; + +enum SX126xPulseShape : uint8_t { + NO_FILTER = 0x00, + GAUSSIAN_BT_0_3 = 0x08, + GAUSSIAN_BT_0_5 = 0x09, + GAUSSIAN_BT_0_7 = 0x0A, + GAUSSIAN_BT_1_0 = 0x0B, +}; + +enum SX126xRampTime : uint8_t { + PA_RAMP_10 = 0x00, + PA_RAMP_20 = 0x01, + PA_RAMP_40 = 0x02, + PA_RAMP_80 = 0x03, + PA_RAMP_200 = 0x04, + PA_RAMP_800 = 0x05, + PA_RAMP_1700 = 0x06, + PA_RAMP_3400 = 0x07, +}; + +} // namespace sx126x +} // namespace esphome diff --git a/esphome/components/sx127x/__init__.py b/esphome/components/sx127x/__init__.py new file mode 100644 index 0000000000..4d034801cc --- /dev/null +++ b/esphome/components/sx127x/__init__.py @@ -0,0 +1,325 @@ +from esphome import automation, pins +import esphome.codegen as cg +from esphome.components import spi +import esphome.config_validation as cv +from esphome.const import CONF_DATA, CONF_FREQUENCY, CONF_ID + +MULTI_CONF = True +CODEOWNERS = ["@swoboda1337"] +DEPENDENCIES = ["spi"] + +CONF_SX127X_ID = "sx127x_id" + +CONF_AUTO_CAL = "auto_cal" +CONF_BANDWIDTH = "bandwidth" +CONF_BITRATE = "bitrate" +CONF_BITSYNC = "bitsync" +CONF_CODING_RATE = "coding_rate" +CONF_CRC_ENABLE = "crc_enable" +CONF_DEVIATION = "deviation" +CONF_DIO0_PIN = "dio0_pin" +CONF_MODULATION = "modulation" +CONF_ON_PACKET = "on_packet" +CONF_PA_PIN = "pa_pin" +CONF_PA_POWER = "pa_power" +CONF_PA_RAMP = "pa_ramp" +CONF_PACKET_MODE = "packet_mode" +CONF_PAYLOAD_LENGTH = "payload_length" +CONF_PREAMBLE_DETECT = "preamble_detect" +CONF_PREAMBLE_ERRORS = "preamble_errors" +CONF_PREAMBLE_POLARITY = "preamble_polarity" +CONF_PREAMBLE_SIZE = "preamble_size" +CONF_RST_PIN = "rst_pin" +CONF_RX_FLOOR = "rx_floor" +CONF_RX_START = "rx_start" +CONF_SHAPING = "shaping" +CONF_SPREADING_FACTOR = "spreading_factor" +CONF_SYNC_VALUE = "sync_value" + +sx127x_ns = cg.esphome_ns.namespace("sx127x") +SX127x = sx127x_ns.class_("SX127x", cg.Component, spi.SPIDevice) +SX127xListener = sx127x_ns.class_("SX127xListener") +SX127xBw = sx127x_ns.enum("SX127xBw") +SX127xOpMode = sx127x_ns.enum("SX127xOpMode") +SX127xPaConfig = sx127x_ns.enum("SX127xPaConfig") +SX127xPaRamp = sx127x_ns.enum("SX127xPaRamp") +SX127xModemCfg1 = sx127x_ns.enum("SX127xModemCfg1") + +BW = { + "2_6kHz": SX127xBw.SX127X_BW_2_6, + "3_1kHz": SX127xBw.SX127X_BW_3_1, + "3_9kHz": SX127xBw.SX127X_BW_3_9, + "5_2kHz": SX127xBw.SX127X_BW_5_2, + "6_3kHz": SX127xBw.SX127X_BW_6_3, + "7_8kHz": SX127xBw.SX127X_BW_7_8, + "10_4kHz": SX127xBw.SX127X_BW_10_4, + "12_5kHz": SX127xBw.SX127X_BW_12_5, + "15_6kHz": SX127xBw.SX127X_BW_15_6, + "20_8kHz": SX127xBw.SX127X_BW_20_8, + "25_0kHz": SX127xBw.SX127X_BW_25_0, + "31_3kHz": SX127xBw.SX127X_BW_31_3, + "41_7kHz": SX127xBw.SX127X_BW_41_7, + "50_0kHz": SX127xBw.SX127X_BW_50_0, + "62_5kHz": SX127xBw.SX127X_BW_62_5, + "83_3kHz": SX127xBw.SX127X_BW_83_3, + "100_0kHz": SX127xBw.SX127X_BW_100_0, + "125_0kHz": SX127xBw.SX127X_BW_125_0, + "166_7kHz": SX127xBw.SX127X_BW_166_7, + "200_0kHz": SX127xBw.SX127X_BW_200_0, + "250_0kHz": SX127xBw.SX127X_BW_250_0, + "500_0kHz": SX127xBw.SX127X_BW_500_0, +} + +CODING_RATE = { + "CR_4_5": SX127xModemCfg1.CODING_RATE_4_5, + "CR_4_6": SX127xModemCfg1.CODING_RATE_4_6, + "CR_4_7": SX127xModemCfg1.CODING_RATE_4_7, + "CR_4_8": SX127xModemCfg1.CODING_RATE_4_8, +} + +MOD = { + "LORA": SX127xOpMode.MOD_LORA, + "FSK": SX127xOpMode.MOD_FSK, + "OOK": SX127xOpMode.MOD_OOK, +} + +PA_PIN = { + "RFO": SX127xPaConfig.PA_PIN_RFO, + "BOOST": SX127xPaConfig.PA_PIN_BOOST, +} + +RAMP = { + "10us": SX127xPaRamp.PA_RAMP_10, + "12us": SX127xPaRamp.PA_RAMP_12, + "15us": SX127xPaRamp.PA_RAMP_15, + "20us": SX127xPaRamp.PA_RAMP_20, + "25us": SX127xPaRamp.PA_RAMP_25, + "31us": SX127xPaRamp.PA_RAMP_31, + "40us": SX127xPaRamp.PA_RAMP_40, + "50us": SX127xPaRamp.PA_RAMP_50, + "62us": SX127xPaRamp.PA_RAMP_62, + "100us": SX127xPaRamp.PA_RAMP_100, + "125us": SX127xPaRamp.PA_RAMP_125, + "250us": SX127xPaRamp.PA_RAMP_250, + "500us": SX127xPaRamp.PA_RAMP_500, + "1000us": SX127xPaRamp.PA_RAMP_1000, + "2000us": SX127xPaRamp.PA_RAMP_2000, + "3400us": SX127xPaRamp.PA_RAMP_3400, +} + +SHAPING = { + "CUTOFF_BR_X_2": SX127xPaRamp.CUTOFF_BR_X_2, + "CUTOFF_BR_X_1": SX127xPaRamp.CUTOFF_BR_X_1, + "GAUSSIAN_BT_0_3": SX127xPaRamp.GAUSSIAN_BT_0_3, + "GAUSSIAN_BT_0_5": SX127xPaRamp.GAUSSIAN_BT_0_5, + "GAUSSIAN_BT_1_0": SX127xPaRamp.GAUSSIAN_BT_1_0, + "NONE": SX127xPaRamp.SHAPING_NONE, +} + +RunImageCalAction = sx127x_ns.class_( + "RunImageCalAction", automation.Action, cg.Parented.template(SX127x) +) +SendPacketAction = sx127x_ns.class_( + "SendPacketAction", automation.Action, cg.Parented.template(SX127x) +) +SetModeTxAction = sx127x_ns.class_( + "SetModeTxAction", automation.Action, cg.Parented.template(SX127x) +) +SetModeRxAction = sx127x_ns.class_( + "SetModeRxAction", automation.Action, cg.Parented.template(SX127x) +) +SetModeSleepAction = sx127x_ns.class_( + "SetModeSleepAction", automation.Action, cg.Parented.template(SX127x) +) +SetModeStandbyAction = sx127x_ns.class_( + "SetModeStandbyAction", automation.Action, cg.Parented.template(SX127x) +) + + +def validate_raw_data(value): + if isinstance(value, str): + return value.encode("utf-8") + if isinstance(value, list): + return cv.Schema([cv.hex_uint8_t])(value) + raise cv.Invalid( + "data must either be a string wrapped in quotes or a list of bytes" + ) + + +def validate_config(config): + if config[CONF_MODULATION] == "LORA": + bws = [ + "7_8kHz", + "10_4kHz", + "15_6kHz", + "20_8kHz", + "31_3kHz", + "41_7kHz", + "62_5kHz", + "125_0kHz", + "250_0kHz", + "500_0kHz", + ] + if config[CONF_BANDWIDTH] not in bws: + raise cv.Invalid(f"{config[CONF_BANDWIDTH]} is not available with LORA") + if CONF_DIO0_PIN not in config: + raise cv.Invalid("Cannot use LoRa without dio0_pin") + if 0 < config[CONF_PREAMBLE_SIZE] < 6: + raise cv.Invalid("Minimum preamble size is 6 with LORA") + 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") + else: + if config[CONF_BANDWIDTH] == "500_0kHz": + raise cv.Invalid(f"{config[CONF_BANDWIDTH]} is only available with LORA") + if CONF_BITSYNC not in config: + raise cv.Invalid("Config 'bitsync' required with FSK/OOK") + if CONF_PACKET_MODE not in config: + raise cv.Invalid("Config 'packet_mode' required with FSK/OOK") + if config[CONF_PACKET_MODE] and CONF_DIO0_PIN not in config: + raise cv.Invalid("Config 'dio0_pin' required in packet mode") + if config[CONF_PAYLOAD_LENGTH] > 64: + raise cv.Invalid("Payload length must be <= 64 with FSK/OOK") + if config[CONF_PA_PIN] == "RFO" and config[CONF_PA_POWER] > 15: + raise cv.Invalid("PA power must be <= 15 dbm when using the RFO pin") + if config[CONF_PA_PIN] == "BOOST" and config[CONF_PA_POWER] < 2: + raise cv.Invalid("PA power must be >= 2 dbm when using the BOOST pin") + return config + + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SX127x), + cv.Optional(CONF_AUTO_CAL, default=True): cv.boolean, + cv.Optional(CONF_BANDWIDTH, default="125_0kHz"): cv.enum(BW), + cv.Optional(CONF_BITRATE, default=4800): cv.int_range(min=500, max=300000), + cv.Optional(CONF_BITSYNC): cv.boolean, + cv.Optional(CONF_CODING_RATE, default="CR_4_5"): cv.enum(CODING_RATE), + cv.Optional(CONF_CRC_ENABLE, default=False): cv.boolean, + cv.Optional(CONF_DEVIATION, default=5000): cv.int_range(min=0, max=100000), + cv.Optional(CONF_DIO0_PIN): pins.internal_gpio_input_pin_schema, + cv.Required(CONF_FREQUENCY): cv.int_range(min=137000000, max=1020000000), + cv.Required(CONF_MODULATION): cv.enum(MOD), + cv.Optional(CONF_ON_PACKET): automation.validate_automation(single=True), + cv.Optional(CONF_PA_PIN, default="BOOST"): cv.enum(PA_PIN), + cv.Optional(CONF_PA_POWER, default=17): cv.int_range(min=0, max=17), + cv.Optional(CONF_PA_RAMP, default="40us"): cv.enum(RAMP), + cv.Optional(CONF_PACKET_MODE): cv.boolean, + cv.Optional(CONF_PAYLOAD_LENGTH, default=0): cv.int_range(min=0, max=256), + cv.Optional(CONF_PREAMBLE_DETECT, default=0): cv.int_range(min=0, max=3), + cv.Optional(CONF_PREAMBLE_ERRORS, default=0): cv.int_range(min=0, max=31), + cv.Optional(CONF_PREAMBLE_POLARITY, default=0xAA): cv.All( + cv.hex_int, cv.one_of(0xAA, 0x55) + ), + cv.Optional(CONF_PREAMBLE_SIZE, default=0): cv.int_range(min=0, max=65535), + cv.Required(CONF_RST_PIN): pins.internal_gpio_output_pin_schema, + cv.Optional(CONF_RX_FLOOR, default=-94): cv.float_range(min=-128, max=-1), + cv.Optional(CONF_RX_START, default=True): cv.boolean, + cv.Optional(CONF_SHAPING, default="NONE"): cv.enum(SHAPING), + cv.Optional(CONF_SPREADING_FACTOR, default=7): cv.int_range(min=6, max=12), + cv.Optional(CONF_SYNC_VALUE, default=[]): cv.ensure_list(cv.hex_uint8_t), + }, + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(spi.spi_device_schema(True, 8e6, "mode0")) + .add_extra(validate_config) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await spi.register_spi_device(var, config) + if CONF_ON_PACKET in config: + await automation.build_automation( + var.get_packet_trigger(), + [ + (cg.std_vector.template(cg.uint8), "x"), + (cg.float_, "rssi"), + (cg.float_, "snr"), + ], + config[CONF_ON_PACKET], + ) + if CONF_DIO0_PIN in config: + dio0_pin = await cg.gpio_pin_expression(config[CONF_DIO0_PIN]) + cg.add(var.set_dio0_pin(dio0_pin)) + rst_pin = await cg.gpio_pin_expression(config[CONF_RST_PIN]) + cg.add(var.set_rst_pin(rst_pin)) + cg.add(var.set_auto_cal(config[CONF_AUTO_CAL])) + cg.add(var.set_bandwidth(config[CONF_BANDWIDTH])) + cg.add(var.set_frequency(config[CONF_FREQUENCY])) + cg.add(var.set_deviation(config[CONF_DEVIATION])) + cg.add(var.set_modulation(config[CONF_MODULATION])) + if config[CONF_MODULATION] != "LORA": + cg.add(var.set_bitrate(config[CONF_BITRATE])) + cg.add(var.set_bitsync(config[CONF_BITSYNC])) + cg.add(var.set_packet_mode(config[CONF_PACKET_MODE])) + cg.add(var.set_pa_pin(config[CONF_PA_PIN])) + cg.add(var.set_pa_ramp(config[CONF_PA_RAMP])) + cg.add(var.set_pa_power(config[CONF_PA_POWER])) + cg.add(var.set_shaping(config[CONF_SHAPING])) + cg.add(var.set_crc_enable(config[CONF_CRC_ENABLE])) + cg.add(var.set_payload_length(config[CONF_PAYLOAD_LENGTH])) + cg.add(var.set_preamble_detect(config[CONF_PREAMBLE_DETECT])) + cg.add(var.set_preamble_size(config[CONF_PREAMBLE_SIZE])) + cg.add(var.set_preamble_polarity(config[CONF_PREAMBLE_POLARITY])) + cg.add(var.set_preamble_errors(config[CONF_PREAMBLE_ERRORS])) + cg.add(var.set_coding_rate(config[CONF_CODING_RATE])) + cg.add(var.set_spreading_factor(config[CONF_SPREADING_FACTOR])) + cg.add(var.set_sync_value(config[CONF_SYNC_VALUE])) + cg.add(var.set_rx_floor(config[CONF_RX_FLOOR])) + cg.add(var.set_rx_start(config[CONF_RX_START])) + + +NO_ARGS_ACTION_SCHEMA = automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(SX127x), + } +) + + +@automation.register_action( + "sx127x.run_image_cal", RunImageCalAction, NO_ARGS_ACTION_SCHEMA +) +@automation.register_action( + "sx127x.set_mode_tx", SetModeTxAction, NO_ARGS_ACTION_SCHEMA +) +@automation.register_action( + "sx127x.set_mode_rx", SetModeRxAction, NO_ARGS_ACTION_SCHEMA +) +@automation.register_action( + "sx127x.set_mode_sleep", SetModeSleepAction, NO_ARGS_ACTION_SCHEMA +) +@automation.register_action( + "sx127x.set_mode_standby", SetModeStandbyAction, NO_ARGS_ACTION_SCHEMA +) +async def no_args_action_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +SEND_PACKET_ACTION_SCHEMA = cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(SX127x), + cv.Required(CONF_DATA): cv.templatable(validate_raw_data), + }, + key=CONF_DATA, +) + + +@automation.register_action( + "sx127x.send_packet", SendPacketAction, SEND_PACKET_ACTION_SCHEMA +) +async def send_packet_action_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + data = config[CONF_DATA] + if isinstance(data, bytes): + data = list(data) + if cg.is_template(data): + templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8)) + cg.add(var.set_data_template(templ)) + else: + cg.add(var.set_data_static(data)) + return var diff --git a/esphome/components/sx127x/automation.h b/esphome/components/sx127x/automation.h new file mode 100644 index 0000000000..2b9c261de1 --- /dev/null +++ b/esphome/components/sx127x/automation.h @@ -0,0 +1,62 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/sx127x/sx127x.h" + +namespace esphome { +namespace sx127x { + +template class RunImageCalAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->run_image_cal(); } +}; + +template class SendPacketAction : public Action, public Parented { + public: + void set_data_template(std::function(Ts...)> func) { + this->data_func_ = func; + this->static_ = false; + } + + void set_data_static(const std::vector &data) { + this->data_static_ = data; + this->static_ = true; + } + + void play(Ts... x) override { + if (this->static_) { + this->parent_->transmit_packet(this->data_static_); + } else { + this->parent_->transmit_packet(this->data_func_(x...)); + } + } + + protected: + bool static_{false}; + std::function(Ts...)> data_func_{}; + std::vector data_static_{}; +}; + +template class SetModeTxAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->set_mode_tx(); } +}; + +template class SetModeRxAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->set_mode_rx(); } +}; + +template class SetModeSleepAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->set_mode_sleep(); } +}; + +template class SetModeStandbyAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->set_mode_standby(); } +}; + +} // namespace sx127x +} // namespace esphome diff --git a/esphome/components/sx127x/packet_transport/__init__.py b/esphome/components/sx127x/packet_transport/__init__.py new file mode 100644 index 0000000000..2f3a0f6e2b --- /dev/null +++ b/esphome/components/sx127x/packet_transport/__init__.py @@ -0,0 +1,26 @@ +import esphome.codegen as cg +from esphome.components.packet_transport import ( + PacketTransport, + new_packet_transport, + transport_schema, +) +import esphome.config_validation as cv +from esphome.cpp_types import PollingComponent + +from .. import CONF_SX127X_ID, SX127x, SX127xListener, sx127x_ns + +SX127xTransport = sx127x_ns.class_( + "SX127xTransport", PacketTransport, PollingComponent, SX127xListener +) + +CONFIG_SCHEMA = transport_schema(SX127xTransport).extend( + { + cv.GenerateID(CONF_SX127X_ID): cv.use_id(SX127x), + } +) + + +async def to_code(config): + var, _ = await new_packet_transport(config) + sx127x = await cg.get_variable(config[CONF_SX127X_ID]) + cg.add(var.set_parent(sx127x)) diff --git a/esphome/components/sx127x/packet_transport/sx127x_transport.cpp b/esphome/components/sx127x/packet_transport/sx127x_transport.cpp new file mode 100644 index 0000000000..b1d014bb96 --- /dev/null +++ b/esphome/components/sx127x/packet_transport/sx127x_transport.cpp @@ -0,0 +1,26 @@ +#include "esphome/core/log.h" +#include "esphome/core/application.h" +#include "sx127x_transport.h" + +namespace esphome { +namespace sx127x { + +static const char *const TAG = "sx127x_transport"; + +void SX127xTransport::setup() { + PacketTransport::setup(); + this->parent_->register_listener(this); +} + +void SX127xTransport::update() { + PacketTransport::update(); + this->updated_ = true; + this->resend_data_ = true; +} + +void SX127xTransport::send_packet(const std::vector &buf) const { this->parent_->transmit_packet(buf); } + +void SX127xTransport::on_packet(const std::vector &packet, float rssi, float snr) { this->process_(packet); } + +} // namespace sx127x +} // namespace esphome diff --git a/esphome/components/sx127x/packet_transport/sx127x_transport.h b/esphome/components/sx127x/packet_transport/sx127x_transport.h new file mode 100644 index 0000000000..e27b7f8d57 --- /dev/null +++ b/esphome/components/sx127x/packet_transport/sx127x_transport.h @@ -0,0 +1,25 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sx127x/sx127x.h" +#include "esphome/components/packet_transport/packet_transport.h" +#include + +namespace esphome { +namespace sx127x { + +class SX127xTransport : public packet_transport::PacketTransport, public Parented, public SX127xListener { + public: + void setup() override; + void update() override; + void on_packet(const std::vector &packet, float rssi, float snr) override; + float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + + protected: + void send_packet(const std::vector &buf) const override; + bool should_send() override { return true; } + size_t get_max_packet_size() override { return this->parent_->get_max_packet_size(); } +}; + +} // namespace sx127x +} // namespace esphome diff --git a/esphome/components/sx127x/sx127x.cpp b/esphome/components/sx127x/sx127x.cpp new file mode 100644 index 0000000000..2d2326549b --- /dev/null +++ b/esphome/components/sx127x/sx127x.cpp @@ -0,0 +1,498 @@ +#include "sx127x.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace sx127x { + +static const char *const TAG = "sx127x"; +static const uint32_t FXOSC = 32000000u; +static const uint16_t RAMP[16] = {3400, 2000, 1000, 500, 250, 125, 100, 62, 50, 40, 31, 25, 20, 15, 12, 10}; +static const uint32_t BW_HZ[22] = {2604, 3125, 3906, 5208, 6250, 7812, 10416, 12500, 15625, 20833, 25000, + 31250, 41666, 50000, 62500, 83333, 100000, 125000, 166666, 200000, 250000, 500000}; +static const uint8_t BW_LORA[22] = {BW_7_8, BW_7_8, BW_7_8, BW_7_8, BW_7_8, BW_7_8, BW_10_4, BW_15_6, + BW_15_6, BW_20_8, BW_31_3, BW_31_3, BW_41_7, BW_62_5, BW_62_5, BW_125_0, + BW_125_0, BW_125_0, BW_250_0, BW_250_0, BW_250_0, BW_500_0}; +static const uint8_t BW_FSK_OOK[22] = {RX_BW_2_6, RX_BW_3_1, RX_BW_3_9, RX_BW_5_2, RX_BW_6_3, RX_BW_7_8, + RX_BW_10_4, RX_BW_12_5, RX_BW_15_6, RX_BW_20_8, RX_BW_25_0, RX_BW_31_3, + RX_BW_41_7, RX_BW_50_0, RX_BW_62_5, RX_BW_83_3, RX_BW_100_0, RX_BW_125_0, + RX_BW_166_7, RX_BW_200_0, RX_BW_250_0, RX_BW_250_0}; +static const int32_t RSSI_OFFSET_HF = 157; +static const int32_t RSSI_OFFSET_LF = 164; + +uint8_t SX127x::read_register_(uint8_t reg) { + this->enable(); + this->write_byte(reg & 0x7F); + uint8_t value = this->read_byte(); + this->disable(); + return value; +} + +void SX127x::write_register_(uint8_t reg, uint8_t value) { + this->enable(); + this->write_byte(reg | 0x80); + this->write_byte(value); + this->disable(); +} + +void SX127x::read_fifo_(std::vector &packet) { + this->enable(); + this->write_byte(REG_FIFO & 0x7F); + this->read_array(packet.data(), packet.size()); + this->disable(); +} + +void SX127x::write_fifo_(const std::vector &packet) { + this->enable(); + this->write_byte(REG_FIFO | 0x80); + this->write_array(packet.data(), packet.size()); + this->disable(); +} + +void SX127x::setup() { + ESP_LOGCONFIG(TAG, "Running setup"); + + // setup reset + this->rst_pin_->setup(); + + // setup dio0 + if (this->dio0_pin_) { + this->dio0_pin_->setup(); + } + + // start spi + this->spi_setup(); + + // configure rf + this->configure(); +} + +void SX127x::configure() { + // toggle chip reset + this->rst_pin_->digital_write(false); + delayMicroseconds(1000); + this->rst_pin_->digital_write(true); + delayMicroseconds(10000); + + // check silicon version to make sure hw is ok + if (this->read_register_(REG_VERSION) != 0x12) { + this->mark_failed(); + return; + } + + // enter sleep mode + this->set_mode_(MOD_FSK, MODE_SLEEP); + + // set freq + uint64_t frf = ((uint64_t) this->frequency_ << 19) / FXOSC; + this->write_register_(REG_FRF_MSB, (uint8_t) ((frf >> 16) & 0xFF)); + this->write_register_(REG_FRF_MID, (uint8_t) ((frf >> 8) & 0xFF)); + this->write_register_(REG_FRF_LSB, (uint8_t) ((frf >> 0) & 0xFF)); + + // enter standby mode + this->set_mode_(MOD_FSK, MODE_STDBY); + + // run image cal + this->run_image_cal(); + + // go back to sleep + this->set_mode_sleep(); + + // config pa + if (this->pa_pin_ == PA_PIN_BOOST) { + this->pa_power_ = std::max(this->pa_power_, (uint8_t) 2); + this->pa_power_ = std::min(this->pa_power_, (uint8_t) 17); + this->write_register_(REG_PA_CONFIG, (this->pa_power_ - 2) | this->pa_pin_ | PA_MAX_POWER); + } else { + this->pa_power_ = std::min(this->pa_power_, (uint8_t) 14); + this->write_register_(REG_PA_CONFIG, (this->pa_power_ - 0) | this->pa_pin_ | PA_MAX_POWER); + } + if (this->modulation_ != MOD_LORA) { + this->write_register_(REG_PA_RAMP, this->pa_ramp_ | this->shaping_); + } else { + this->write_register_(REG_PA_RAMP, this->pa_ramp_); + } + + // configure modem + if (this->modulation_ != MOD_LORA) { + this->configure_fsk_ook_(); + } else { + this->configure_lora_(); + } + + // switch to rx or sleep + if (this->rx_start_) { + this->set_mode_rx(); + } else { + this->set_mode_sleep(); + } +} + +void SX127x::configure_fsk_ook_() { + // set the channel bw + this->write_register_(REG_RX_BW, BW_FSK_OOK[this->bandwidth_]); + + // set fdev + uint32_t fdev = std::min((this->deviation_ * 4096) / 250000, (uint32_t) 0x3FFF); + this->write_register_(REG_FDEV_MSB, (uint8_t) ((fdev >> 8) & 0xFF)); + this->write_register_(REG_FDEV_LSB, (uint8_t) ((fdev >> 0) & 0xFF)); + + // set bitrate + uint64_t bitrate = (FXOSC + this->bitrate_ / 2) / this->bitrate_; // round up + this->write_register_(REG_BITRATE_MSB, (uint8_t) ((bitrate >> 8) & 0xFF)); + this->write_register_(REG_BITRATE_LSB, (uint8_t) ((bitrate >> 0) & 0xFF)); + + // configure rx and afc + uint8_t trigger = (this->preamble_detect_ > 0) ? TRIGGER_PREAMBLE : TRIGGER_RSSI; + this->write_register_(REG_AFC_FEI, AFC_AUTO_CLEAR_ON); + if (this->modulation_ == MOD_FSK) { + this->write_register_(REG_RX_CONFIG, AFC_AUTO_ON | AGC_AUTO_ON | trigger); + } else { + this->write_register_(REG_RX_CONFIG, AGC_AUTO_ON | trigger); + } + + // configure packet mode + if (this->packet_mode_) { + uint8_t crc_mode = (this->crc_enable_) ? CRC_ON : CRC_OFF; + this->write_register_(REG_FIFO_THRESH, TX_START_FIFO_EMPTY); + if (this->payload_length_ > 0) { + this->write_register_(REG_PAYLOAD_LENGTH_LSB, this->payload_length_); + this->write_register_(REG_PACKET_CONFIG_1, crc_mode | FIXED_LENGTH); + } else { + this->write_register_(REG_PAYLOAD_LENGTH_LSB, this->get_max_packet_size() - 1); + this->write_register_(REG_PACKET_CONFIG_1, crc_mode | VARIABLE_LENGTH); + } + this->write_register_(REG_PACKET_CONFIG_2, PACKET_MODE); + } else { + this->write_register_(REG_PACKET_CONFIG_2, CONTINUOUS_MODE); + } + this->write_register_(REG_DIO_MAPPING1, DIO0_MAPPING_00); + + // config bit synchronizer + uint8_t polarity = (this->preamble_polarity_ == 0xAA) ? PREAMBLE_AA : PREAMBLE_55; + if (!this->sync_value_.empty()) { + uint8_t size = this->sync_value_.size() - 1; + this->write_register_(REG_SYNC_CONFIG, AUTO_RESTART_PLL_LOCK | polarity | SYNC_ON | size); + for (uint32_t i = 0; i < this->sync_value_.size(); i++) { + this->write_register_(REG_SYNC_VALUE1 + i, this->sync_value_[i]); + } + } else { + this->write_register_(REG_SYNC_CONFIG, AUTO_RESTART_PLL_LOCK | polarity); + } + + // config preamble detector + if (this->preamble_detect_ > 0) { + uint8_t size = (this->preamble_detect_ - 1) << PREAMBLE_DETECTOR_SIZE_SHIFT; + uint8_t tol = this->preamble_errors_ << PREAMBLE_DETECTOR_TOL_SHIFT; + this->write_register_(REG_PREAMBLE_DETECT, PREAMBLE_DETECTOR_ON | size | tol); + } else { + this->write_register_(REG_PREAMBLE_DETECT, PREAMBLE_DETECTOR_OFF); + } + this->write_register_(REG_PREAMBLE_SIZE_MSB, this->preamble_size_ >> 16); + this->write_register_(REG_PREAMBLE_SIZE_LSB, this->preamble_size_ & 0xFF); + + // config sync generation and setup ook threshold + uint8_t bitsync = this->bitsync_ ? BIT_SYNC_ON : BIT_SYNC_OFF; + this->write_register_(REG_OOK_PEAK, bitsync | OOK_THRESH_STEP_0_5 | OOK_THRESH_PEAK); + this->write_register_(REG_OOK_AVG, OOK_AVG_RESERVED | OOK_THRESH_DEC_1_8); + + // set rx floor + this->write_register_(REG_OOK_FIX, 256 + int(this->rx_floor_ * 2.0)); + this->write_register_(REG_RSSI_THRESH, std::abs(int(this->rx_floor_ * 2.0))); +} + +void SX127x::configure_lora_() { + // config modem + uint8_t header_mode = this->payload_length_ > 0 ? IMPLICIT_HEADER : EXPLICIT_HEADER; + uint8_t crc_mode = (this->crc_enable_) ? RX_PAYLOAD_CRC_ON : RX_PAYLOAD_CRC_OFF; + uint8_t spreading_factor = this->spreading_factor_ << SPREADING_FACTOR_SHIFT; + this->write_register_(REG_MODEM_CONFIG1, BW_LORA[this->bandwidth_] | this->coding_rate_ | header_mode); + this->write_register_(REG_MODEM_CONFIG2, spreading_factor | crc_mode); + + // config fifo and payload length + this->write_register_(REG_FIFO_TX_BASE_ADDR, 0x00); + this->write_register_(REG_FIFO_RX_BASE_ADDR, 0x00); + this->write_register_(REG_PAYLOAD_LENGTH, std::max(this->payload_length_, (uint32_t) 1)); + + // config preamble + if (this->preamble_size_ >= 6) { + this->write_register_(REG_PREAMBLE_LEN_MSB, this->preamble_size_ >> 16); + this->write_register_(REG_PREAMBLE_LEN_LSB, this->preamble_size_ & 0xFF); + } + + // optimize detection + float duration = 1000.0f * std::pow(2, this->spreading_factor_) / BW_HZ[this->bandwidth_]; + if (duration > 16) { + this->write_register_(REG_MODEM_CONFIG3, MODEM_AGC_AUTO_ON | LOW_DATA_RATE_OPTIMIZE_ON); + } else { + this->write_register_(REG_MODEM_CONFIG3, MODEM_AGC_AUTO_ON); + } + if (this->spreading_factor_ == 6) { + this->write_register_(REG_DETECT_OPTIMIZE, 0xC5); + this->write_register_(REG_DETECT_THRESHOLD, 0x0C); + } else { + this->write_register_(REG_DETECT_OPTIMIZE, 0xC3); + this->write_register_(REG_DETECT_THRESHOLD, 0x0A); + } + + // config sync word + if (!this->sync_value_.empty()) { + this->write_register_(REG_SYNC_WORD, this->sync_value_[0]); + } +} + +size_t SX127x::get_max_packet_size() { + if (this->payload_length_ > 0) { + return this->payload_length_; + } + if (this->modulation_ == MOD_LORA) { + return 256; + } else { + return 64; + } +} + +SX127xError SX127x::transmit_packet(const std::vector &packet) { + if (this->payload_length_ > 0 && this->payload_length_ != packet.size()) { + ESP_LOGE(TAG, "Packet size does not match config"); + return SX127xError::INVALID_PARAMS; + } + if (packet.empty() || packet.size() > this->get_max_packet_size()) { + ESP_LOGE(TAG, "Packet size out of range"); + return SX127xError::INVALID_PARAMS; + } + + SX127xError ret = SX127xError::NONE; + if (this->modulation_ == MOD_LORA) { + this->set_mode_standby(); + if (this->payload_length_ == 0) { + this->write_register_(REG_PAYLOAD_LENGTH, packet.size()); + } + this->write_register_(REG_IRQ_FLAGS, 0xFF); + this->write_register_(REG_FIFO_ADDR_PTR, 0); + this->write_fifo_(packet); + this->set_mode_tx(); + } else { + this->set_mode_standby(); + if (this->payload_length_ == 0) { + this->write_register_(REG_FIFO, packet.size()); + } + this->write_fifo_(packet); + this->set_mode_tx(); + } + + // wait until transmit completes, typically the delay will be less than 100 ms + uint32_t start = millis(); + while (!this->dio0_pin_->digital_read()) { + if (millis() - start > 4000) { + ESP_LOGE(TAG, "Transmit packet failure"); + ret = SX127xError::TIMEOUT; + break; + } + } + if (this->rx_start_) { + this->set_mode_rx(); + } else { + this->set_mode_sleep(); + } + return ret; +} + +void SX127x::call_listeners_(const std::vector &packet, float rssi, float snr) { + for (auto &listener : this->listeners_) { + listener->on_packet(packet, rssi, snr); + } + this->packet_trigger_->trigger(packet, rssi, snr); +} + +void SX127x::loop() { + if (this->dio0_pin_ == nullptr || !this->dio0_pin_->digital_read()) { + return; + } + + if (this->modulation_ == MOD_LORA) { + uint8_t status = this->read_register_(REG_IRQ_FLAGS); + this->write_register_(REG_IRQ_FLAGS, 0xFF); + if ((status & PAYLOAD_CRC_ERROR) == 0) { + uint8_t bytes = this->read_register_(REG_NB_RX_BYTES); + uint8_t addr = this->read_register_(REG_FIFO_RX_CURR_ADDR); + uint8_t rssi = this->read_register_(REG_PKT_RSSI_VALUE); + int8_t snr = (int8_t) this->read_register_(REG_PKT_SNR_VALUE); + this->packet_.resize(bytes); + this->write_register_(REG_FIFO_ADDR_PTR, addr); + this->read_fifo_(this->packet_); + if (this->frequency_ > 700000000) { + this->call_listeners_(this->packet_, (float) rssi - RSSI_OFFSET_HF, (float) snr / 4); + } else { + this->call_listeners_(this->packet_, (float) rssi - RSSI_OFFSET_LF, (float) snr / 4); + } + } + } else if (this->packet_mode_) { + uint8_t payload_length = this->payload_length_; + if (payload_length == 0) { + payload_length = this->read_register_(REG_FIFO); + } + this->packet_.resize(payload_length); + this->read_fifo_(this->packet_); + this->call_listeners_(this->packet_, 0.0f, 0.0f); + } +} + +void SX127x::run_image_cal() { + if (this->modulation_ == MOD_LORA) { + this->set_mode_(MOD_FSK, MODE_SLEEP); + this->set_mode_(MOD_FSK, MODE_STDBY); + } + if (this->auto_cal_) { + this->write_register_(REG_IMAGE_CAL, IMAGE_CAL_START | AUTO_IMAGE_CAL_ON | TEMP_THRESHOLD_10C); + } else { + this->write_register_(REG_IMAGE_CAL, IMAGE_CAL_START); + } + uint32_t start = millis(); + while (this->read_register_(REG_IMAGE_CAL) & IMAGE_CAL_RUNNING) { + if (millis() - start > 20) { + ESP_LOGE(TAG, "Image cal failure"); + this->mark_failed(); + break; + } + } + if (this->modulation_ == MOD_LORA) { + this->set_mode_(this->modulation_, MODE_SLEEP); + this->set_mode_(this->modulation_, MODE_STDBY); + } +} + +void SX127x::set_mode_(uint8_t modulation, uint8_t mode) { + uint32_t start = millis(); + this->write_register_(REG_OP_MODE, modulation | mode); + while (true) { + uint8_t curr = this->read_register_(REG_OP_MODE) & MODE_MASK; + if ((curr == mode) || (mode == MODE_RX && curr == MODE_RX_FS)) { + if (mode == MODE_SLEEP) { + this->write_register_(REG_OP_MODE, modulation | mode); + } + break; + } + if (millis() - start > 20) { + ESP_LOGE(TAG, "Set mode failure"); + this->mark_failed(); + break; + } + } +} + +void SX127x::set_mode_rx() { + this->set_mode_(this->modulation_, MODE_RX); + if (this->modulation_ == MOD_LORA) { + this->write_register_(REG_IRQ_FLAGS_MASK, 0x00); + this->write_register_(REG_DIO_MAPPING1, DIO0_MAPPING_00); + } +} + +void SX127x::set_mode_tx() { + this->set_mode_(this->modulation_, MODE_TX); + if (this->modulation_ == MOD_LORA) { + this->write_register_(REG_IRQ_FLAGS_MASK, 0x00); + this->write_register_(REG_DIO_MAPPING1, DIO0_MAPPING_01); + } +} + +void SX127x::set_mode_standby() { this->set_mode_(this->modulation_, MODE_STDBY); } + +void SX127x::set_mode_sleep() { this->set_mode_(this->modulation_, MODE_SLEEP); } + +void SX127x::dump_config() { + ESP_LOGCONFIG(TAG, "SX127x:"); + LOG_PIN(" CS Pin: ", this->cs_); + LOG_PIN(" RST Pin: ", this->rst_pin_); + LOG_PIN(" DIO0 Pin: ", this->dio0_pin_); + const char *pa_pin = "RFO"; + if (this->pa_pin_ == PA_PIN_BOOST) { + pa_pin = "BOOST"; + } + ESP_LOGCONFIG(TAG, + " Auto Cal: %s\n" + " Frequency: %" PRIu32 " Hz\n" + " Bandwidth: %" PRIu32 " Hz\n" + " PA Pin: %s\n" + " PA Power: %" PRIu8 " dBm\n" + " PA Ramp: %" PRIu16 " us", + TRUEFALSE(this->auto_cal_), this->frequency_, BW_HZ[this->bandwidth_], pa_pin, this->pa_power_, + RAMP[this->pa_ramp_]); + if (this->modulation_ == MOD_FSK) { + ESP_LOGCONFIG(TAG, " Deviation: %" PRIu32 " Hz", this->deviation_); + } + if (this->modulation_ == MOD_LORA) { + const char *cr = "4/8"; + if (this->coding_rate_ == CODING_RATE_4_5) { + cr = "4/5"; + } else if (this->coding_rate_ == CODING_RATE_4_6) { + cr = "4/6"; + } else if (this->coding_rate_ == CODING_RATE_4_7) { + cr = "4/7"; + } + ESP_LOGCONFIG(TAG, + " Modulation: LORA\n" + " Preamble Size: %" PRIu16 "\n" + " Spreading Factor: %" PRIu8 "\n" + " Coding Rate: %s\n" + " CRC Enable: %s", + this->preamble_size_, this->spreading_factor_, cr, TRUEFALSE(this->crc_enable_)); + if (this->payload_length_ > 0) { + ESP_LOGCONFIG(TAG, " Payload Length: %" PRIu32, this->payload_length_); + } + if (!this->sync_value_.empty()) { + ESP_LOGCONFIG(TAG, " Sync Value: 0x%02x", this->sync_value_[0]); + } + } else { + const char *shaping = "NONE"; + if (this->modulation_ == MOD_FSK) { + if (this->shaping_ == GAUSSIAN_BT_0_3) { + shaping = "GAUSSIAN_BT_0_3"; + } else if (this->shaping_ == GAUSSIAN_BT_0_5) { + shaping = "GAUSSIAN_BT_0_5"; + } else if (this->shaping_ == GAUSSIAN_BT_1_0) { + shaping = "GAUSSIAN_BT_1_0"; + } + } else { + if (this->shaping_ == CUTOFF_BR_X_2) { + shaping = "CUTOFF_BR_X_2"; + } else if (this->shaping_ == CUTOFF_BR_X_1) { + shaping = "CUTOFF_BR_X_1"; + } + } + ESP_LOGCONFIG(TAG, + " Shaping: %s\n" + " Modulation: %s\n" + " Bitrate: %" PRIu32 "b/s\n" + " Bitsync: %s\n" + " Rx Start: %s\n" + " Rx Floor: %.1f dBm\n" + " Packet Mode: %s", + shaping, this->modulation_ == MOD_FSK ? "FSK" : "OOK", this->bitrate_, TRUEFALSE(this->bitsync_), + TRUEFALSE(this->rx_start_), this->rx_floor_, TRUEFALSE(this->packet_mode_)); + if (this->packet_mode_) { + ESP_LOGCONFIG(TAG, " CRC Enable: %s", TRUEFALSE(this->crc_enable_)); + } + if (this->payload_length_ > 0) { + ESP_LOGCONFIG(TAG, " Payload Length: %" PRIu32, this->payload_length_); + } + if (!this->sync_value_.empty()) { + ESP_LOGCONFIG(TAG, " Sync Value: 0x%s", format_hex(this->sync_value_).c_str()); + } + if (this->preamble_size_ > 0 || this->preamble_detect_ > 0) { + ESP_LOGCONFIG(TAG, + " Preamble Polarity: 0x%X\n" + " Preamble Size: %" PRIu16 "\n" + " Preamble Detect: %" PRIu8 "\n" + " Preamble Errors: %" PRIu8, + this->preamble_polarity_, this->preamble_size_, this->preamble_detect_, this->preamble_errors_); + } + } + if (this->is_failed()) { + ESP_LOGE(TAG, "Configuring SX127x failed"); + } +} + +} // namespace sx127x +} // namespace esphome diff --git a/esphome/components/sx127x/sx127x.h b/esphome/components/sx127x/sx127x.h new file mode 100644 index 0000000000..0600b51201 --- /dev/null +++ b/esphome/components/sx127x/sx127x.h @@ -0,0 +1,128 @@ +#pragma once + +#include "sx127x_reg.h" +#include "esphome/components/spi/spi.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include + +namespace esphome { +namespace sx127x { + +enum SX127xBw : uint8_t { + SX127X_BW_2_6, + SX127X_BW_3_1, + SX127X_BW_3_9, + SX127X_BW_5_2, + SX127X_BW_6_3, + SX127X_BW_7_8, + SX127X_BW_10_4, + SX127X_BW_12_5, + SX127X_BW_15_6, + SX127X_BW_20_8, + SX127X_BW_25_0, + SX127X_BW_31_3, + SX127X_BW_41_7, + SX127X_BW_50_0, + SX127X_BW_62_5, + SX127X_BW_83_3, + SX127X_BW_100_0, + SX127X_BW_125_0, + SX127X_BW_166_7, + SX127X_BW_200_0, + SX127X_BW_250_0, + SX127X_BW_500_0, +}; + +enum class SX127xError { NONE = 0, TIMEOUT, INVALID_PARAMS }; + +class SX127xListener { + public: + virtual void on_packet(const std::vector &packet, float rssi, float snr) = 0; +}; + +class SX127x : public Component, + public spi::SPIDevice { + public: + size_t get_max_packet_size(); + float get_setup_priority() const override { return setup_priority::PROCESSOR; } + void setup() override; + void loop() override; + void dump_config() override; + void set_auto_cal(bool auto_cal) { this->auto_cal_ = auto_cal; } + void set_bandwidth(SX127xBw bandwidth) { this->bandwidth_ = bandwidth; } + void set_bitrate(uint32_t bitrate) { this->bitrate_ = bitrate; } + void set_bitsync(bool bitsync) { this->bitsync_ = bitsync; } + void set_coding_rate(uint8_t coding_rate) { this->coding_rate_ = coding_rate; } + void set_crc_enable(bool crc_enable) { this->crc_enable_ = crc_enable; } + void set_deviation(uint32_t deviation) { this->deviation_ = deviation; } + void set_dio0_pin(InternalGPIOPin *dio0_pin) { this->dio0_pin_ = dio0_pin; } + void set_frequency(uint32_t frequency) { this->frequency_ = frequency; } + void set_mode_rx(); + void set_mode_tx(); + void set_mode_standby(); + void set_mode_sleep(); + void set_modulation(uint8_t modulation) { this->modulation_ = modulation; } + void set_pa_pin(uint8_t pin) { this->pa_pin_ = pin; } + void set_pa_power(uint8_t power) { this->pa_power_ = power; } + void set_pa_ramp(uint8_t ramp) { this->pa_ramp_ = ramp; } + void set_packet_mode(bool packet_mode) { this->packet_mode_ = packet_mode; } + void set_payload_length(uint8_t payload_length) { this->payload_length_ = payload_length; } + void set_preamble_errors(uint8_t preamble_errors) { this->preamble_errors_ = preamble_errors; } + void set_preamble_polarity(uint8_t preamble_polarity) { this->preamble_polarity_ = preamble_polarity; } + void set_preamble_size(uint16_t preamble_size) { this->preamble_size_ = preamble_size; } + void set_preamble_detect(uint8_t preamble_detect) { this->preamble_detect_ = preamble_detect; } + void set_rst_pin(InternalGPIOPin *rst_pin) { this->rst_pin_ = rst_pin; } + void set_rx_floor(float floor) { this->rx_floor_ = floor; } + void set_rx_start(bool start) { this->rx_start_ = start; } + void set_shaping(uint8_t shaping) { this->shaping_ = shaping; } + void set_spreading_factor(uint8_t spreading_factor) { this->spreading_factor_ = spreading_factor; } + void set_sync_value(const std::vector &sync_value) { this->sync_value_ = sync_value; } + void run_image_cal(); + void configure(); + SX127xError transmit_packet(const std::vector &packet); + void register_listener(SX127xListener *listener) { this->listeners_.push_back(listener); } + Trigger, float, float> *get_packet_trigger() const { return this->packet_trigger_; }; + + protected: + void configure_fsk_ook_(); + void configure_lora_(); + void set_mode_(uint8_t modulation, uint8_t mode); + void write_fifo_(const std::vector &packet); + void read_fifo_(std::vector &packet); + void write_register_(uint8_t reg, uint8_t value); + void call_listeners_(const std::vector &packet, float rssi, float snr); + uint8_t read_register_(uint8_t reg); + Trigger, float, float> *packet_trigger_{new Trigger, float, float>()}; + std::vector listeners_; + std::vector packet_; + std::vector sync_value_; + InternalGPIOPin *dio0_pin_{nullptr}; + InternalGPIOPin *rst_pin_{nullptr}; + SX127xBw bandwidth_; + uint32_t bitrate_; + uint32_t deviation_; + uint32_t frequency_; + uint32_t payload_length_; + uint16_t preamble_size_; + uint8_t coding_rate_; + uint8_t modulation_; + uint8_t pa_pin_; + uint8_t pa_power_; + uint8_t pa_ramp_; + uint8_t preamble_detect_; + uint8_t preamble_errors_; + uint8_t preamble_polarity_; + uint8_t shaping_; + uint8_t spreading_factor_; + float rx_floor_; + bool auto_cal_{false}; + bool bitsync_{false}; + bool crc_enable_{false}; + bool packet_mode_{false}; + bool rx_start_{false}; +}; + +} // namespace sx127x +} // namespace esphome diff --git a/esphome/components/sx127x/sx127x_reg.h b/esphome/components/sx127x/sx127x_reg.h new file mode 100644 index 0000000000..d5e9c50957 --- /dev/null +++ b/esphome/components/sx127x/sx127x_reg.h @@ -0,0 +1,295 @@ +#pragma once + +#include "esphome/core/hal.h" + +namespace esphome { +namespace sx127x { + +enum SX127xReg : uint8_t { + // Common registers + REG_FIFO = 0x00, + REG_OP_MODE = 0x01, + REG_BITRATE_MSB = 0x02, + REG_BITRATE_LSB = 0x03, + REG_FDEV_MSB = 0x04, + REG_FDEV_LSB = 0x05, + REG_FRF_MSB = 0x06, + REG_FRF_MID = 0x07, + REG_FRF_LSB = 0x08, + REG_PA_CONFIG = 0x09, + REG_PA_RAMP = 0x0A, + REG_DIO_MAPPING1 = 0x40, + REG_DIO_MAPPING2 = 0x41, + REG_VERSION = 0x42, + // FSK/OOK registers + REG_RX_CONFIG = 0x0D, + REG_RSSI_THRESH = 0x10, + REG_RX_BW = 0x12, + REG_OOK_PEAK = 0x14, + REG_OOK_FIX = 0x15, + REG_OOK_AVG = 0x16, + REG_AFC_FEI = 0x1A, + REG_PREAMBLE_DETECT = 0x1F, + REG_PREAMBLE_SIZE_MSB = 0x25, + REG_PREAMBLE_SIZE_LSB = 0x26, + REG_SYNC_CONFIG = 0x27, + REG_SYNC_VALUE1 = 0x28, + REG_SYNC_VALUE2 = 0x29, + REG_SYNC_VALUE3 = 0x2A, + REG_SYNC_VALUE4 = 0x2B, + REG_SYNC_VALUE5 = 0x2C, + REG_SYNC_VALUE6 = 0x2D, + REG_SYNC_VALUE7 = 0x2E, + REG_SYNC_VALUE8 = 0x2F, + REG_PACKET_CONFIG_1 = 0x30, + REG_PACKET_CONFIG_2 = 0x31, + REG_PAYLOAD_LENGTH_LSB = 0x32, + REG_FIFO_THRESH = 0x35, + REG_IMAGE_CAL = 0x3B, + // LoRa registers + REG_FIFO_ADDR_PTR = 0x0D, + REG_FIFO_TX_BASE_ADDR = 0x0E, + REG_FIFO_RX_BASE_ADDR = 0x0F, + REG_FIFO_RX_CURR_ADDR = 0x10, + REG_IRQ_FLAGS_MASK = 0x11, + REG_IRQ_FLAGS = 0x12, + REG_NB_RX_BYTES = 0x13, + REG_MODEM_STAT = 0x18, + REG_PKT_SNR_VALUE = 0x19, + REG_PKT_RSSI_VALUE = 0x1A, + REG_RSSI_VALUE = 0x1B, + REG_HOP_CHANNEL = 0x1C, + REG_MODEM_CONFIG1 = 0x1D, + REG_MODEM_CONFIG2 = 0x1E, + REG_SYMB_TIMEOUT_LSB = 0x1F, + REG_PREAMBLE_LEN_MSB = 0x20, + REG_PREAMBLE_LEN_LSB = 0x21, + REG_PAYLOAD_LENGTH = 0x22, + REG_HOP_PERIOD = 0x24, + REG_FIFO_RX_BYTE_ADDR = 0x25, + REG_MODEM_CONFIG3 = 0x26, + REG_FEI_MSB = 0x28, + REG_FEI_MIB = 0x29, + REG_FEI_LSB = 0x2A, + REG_DETECT_OPTIMIZE = 0x31, + REG_INVERT_IQ = 0x33, + REG_DETECT_THRESHOLD = 0x37, + REG_SYNC_WORD = 0x39, +}; + +enum SX127xOpMode : uint8_t { + MOD_LORA = 0x80, + ACCESS_FSK_REGS = 0x40, + ACCESS_LORA_REGS = 0x00, + MOD_OOK = 0x20, + MOD_FSK = 0x00, + ACCESS_LF_REGS = 0x08, + ACCESS_HF_REGS = 0x00, + MODE_CAD = 0x07, + MODE_RX_SINGLE = 0x06, + MODE_RX = 0x05, + MODE_RX_FS = 0x04, + MODE_TX = 0x03, + MODE_TX_FS = 0x02, + MODE_STDBY = 0x01, + MODE_SLEEP = 0x00, + MODE_MASK = 0x07, +}; + +enum SX127xPaConfig : uint8_t { + PA_PIN_BOOST = 0x80, + PA_PIN_RFO = 0x00, + PA_MAX_POWER = 0x70, +}; + +enum SX127xPaRamp : uint8_t { + CUTOFF_BR_X_2 = 0x40, + CUTOFF_BR_X_1 = 0x20, + GAUSSIAN_BT_0_3 = 0x60, + GAUSSIAN_BT_0_5 = 0x40, + GAUSSIAN_BT_1_0 = 0x20, + SHAPING_NONE = 0x00, + PA_RAMP_10 = 0x0F, + PA_RAMP_12 = 0x0E, + PA_RAMP_15 = 0x0D, + PA_RAMP_20 = 0x0C, + PA_RAMP_25 = 0x0B, + PA_RAMP_31 = 0x0A, + PA_RAMP_40 = 0x09, + PA_RAMP_50 = 0x08, + PA_RAMP_62 = 0x07, + PA_RAMP_100 = 0x06, + PA_RAMP_125 = 0x05, + PA_RAMP_250 = 0x04, + PA_RAMP_500 = 0x03, + PA_RAMP_1000 = 0x02, + PA_RAMP_2000 = 0x01, + PA_RAMP_3400 = 0x00, +}; + +enum SX127xDioMapping1 : uint8_t { + DIO0_MAPPING_00 = 0x00, + DIO0_MAPPING_01 = 0x40, + DIO0_MAPPING_10 = 0x80, + DIO0_MAPPING_11 = 0xC0, +}; + +enum SX127xRxConfig : uint8_t { + RESTART_ON_COLLISION = 0x80, + RESTART_NO_LOCK = 0x40, + RESTART_PLL_LOCK = 0x20, + AFC_AUTO_ON = 0x10, + AGC_AUTO_ON = 0x08, + TRIGGER_NONE = 0x00, + TRIGGER_RSSI = 0x01, + TRIGGER_PREAMBLE = 0x06, + TRIGGER_ALL = 0x07, +}; + +enum SX127xRxBw : uint8_t { + RX_BW_2_6 = 0x17, + RX_BW_3_1 = 0x0F, + RX_BW_3_9 = 0x07, + RX_BW_5_2 = 0x16, + RX_BW_6_3 = 0x0E, + RX_BW_7_8 = 0x06, + RX_BW_10_4 = 0x15, + RX_BW_12_5 = 0x0D, + RX_BW_15_6 = 0x05, + RX_BW_20_8 = 0x14, + RX_BW_25_0 = 0x0C, + RX_BW_31_3 = 0x04, + RX_BW_41_7 = 0x13, + RX_BW_50_0 = 0x0B, + RX_BW_62_5 = 0x03, + RX_BW_83_3 = 0x12, + RX_BW_100_0 = 0x0A, + RX_BW_125_0 = 0x02, + RX_BW_166_7 = 0x11, + RX_BW_200_0 = 0x09, + RX_BW_250_0 = 0x01, +}; + +enum SX127xOokPeak : uint8_t { + BIT_SYNC_ON = 0x20, + BIT_SYNC_OFF = 0x00, + OOK_THRESH_AVG = 0x10, + OOK_THRESH_PEAK = 0x08, + OOK_THRESH_FIXED = 0x00, + OOK_THRESH_STEP_6_0 = 0x07, + OOK_THRESH_STEP_5_0 = 0x06, + OOK_THRESH_STEP_4_0 = 0x05, + OOK_THRESH_STEP_3_0 = 0x04, + OOK_THRESH_STEP_2_0 = 0x03, + OOK_THRESH_STEP_1_5 = 0x02, + OOK_THRESH_STEP_1_0 = 0x01, + OOK_THRESH_STEP_0_5 = 0x00, +}; + +enum SX127xOokAvg : uint8_t { + OOK_THRESH_DEC_16 = 0xE0, + OOK_THRESH_DEC_8 = 0xC0, + OOK_THRESH_DEC_4 = 0xA0, + OOK_THRESH_DEC_2 = 0x80, + OOK_THRESH_DEC_1_8 = 0x60, + OOK_THRESH_DEC_1_4 = 0x40, + OOK_THRESH_DEC_1_2 = 0x20, + OOK_THRESH_DEC_1 = 0x00, + OOK_AVG_RESERVED = 0x10, +}; + +enum SX127xAfcFei : uint8_t { + AFC_AUTO_CLEAR_ON = 0x01, +}; + +enum SX127xPreambleDetect : uint8_t { + PREAMBLE_DETECTOR_ON = 0x80, + PREAMBLE_DETECTOR_OFF = 0x00, + PREAMBLE_DETECTOR_SIZE_SHIFT = 5, + PREAMBLE_DETECTOR_TOL_SHIFT = 0, +}; + +enum SX127xSyncConfig : uint8_t { + AUTO_RESTART_PLL_LOCK = 0x80, + AUTO_RESTART_NO_LOCK = 0x40, + AUTO_RESTART_OFF = 0x00, + PREAMBLE_55 = 0x20, + PREAMBLE_AA = 0x00, + SYNC_ON = 0x10, + SYNC_OFF = 0x00, +}; + +enum SX127xPacketConfig1 : uint8_t { + VARIABLE_LENGTH = 0x80, + FIXED_LENGTH = 0x00, + CRC_ON = 0x10, + CRC_OFF = 0x00, +}; + +enum SX127xPacketConfig2 : uint8_t { + CONTINUOUS_MODE = 0x00, + PACKET_MODE = 0x40, +}; + +enum SX127xFifoThresh : uint8_t { + TX_START_FIFO_EMPTY = 0x80, + TX_START_FIFO_LEVEL = 0x00, +}; + +enum SX127xImageCal : uint8_t { + AUTO_IMAGE_CAL_ON = 0x80, + IMAGE_CAL_START = 0x40, + IMAGE_CAL_RUNNING = 0x20, + TEMP_CHANGE = 0x08, + TEMP_THRESHOLD_20C = 0x06, + TEMP_THRESHOLD_15C = 0x04, + TEMP_THRESHOLD_10C = 0x02, + TEMP_THRESHOLD_5C = 0x00, + TEMP_MONITOR_OFF = 0x01, + TEMP_MONITOR_ON = 0x00, +}; + +enum SX127xIrqFlags : uint8_t { + RX_TIMEOUT = 0x80, + RX_DONE = 0x40, + PAYLOAD_CRC_ERROR = 0x20, + VALID_HEADER = 0x10, + TX_DONE = 0x08, + CAD_DONE = 0x04, + FHSS_CHANGE_CHANNEL = 0x02, + CAD_DETECTED = 0x01, +}; + +enum SX127xModemCfg1 : uint8_t { + BW_7_8 = 0x00, + BW_10_4 = 0x10, + BW_15_6 = 0x20, + BW_20_8 = 0x30, + BW_31_3 = 0x40, + BW_41_7 = 0x50, + BW_62_5 = 0x60, + BW_125_0 = 0x70, + BW_250_0 = 0x80, + BW_500_0 = 0x90, + CODING_RATE_4_5 = 0x02, + CODING_RATE_4_6 = 0x04, + CODING_RATE_4_7 = 0x06, + CODING_RATE_4_8 = 0x08, + IMPLICIT_HEADER = 0x01, + EXPLICIT_HEADER = 0x00, +}; + +enum SX127xModemCfg2 : uint8_t { + SPREADING_FACTOR_SHIFT = 4, + TX_CONTINOUS_MODE = 0x08, + RX_PAYLOAD_CRC_ON = 0x04, + RX_PAYLOAD_CRC_OFF = 0x00, +}; + +enum SX127xModemCfg3 : uint8_t { + LOW_DATA_RATE_OPTIMIZE_ON = 0x08, + MODEM_AGC_AUTO_ON = 0x04, +}; + +} // namespace sx127x +} // namespace esphome diff --git a/esphome/components/syslog/esphome_syslog.cpp b/esphome/components/syslog/esphome_syslog.cpp index 9d2cda549b..e322a6951d 100644 --- a/esphome/components/syslog/esphome_syslog.cpp +++ b/esphome/components/syslog/esphome_syslog.cpp @@ -21,10 +21,12 @@ constexpr int LOG_LEVEL_TO_SYSLOG_SEVERITY[] = { void Syslog::setup() { logger::global_logger->add_on_log_callback( - [this](int level, const char *tag, const char *message) { this->log_(level, tag, message); }); + [this](int level, const char *tag, const char *message, size_t message_len) { + this->log_(level, tag, message, message_len); + }); } -void Syslog::log_(const int level, const char *tag, const char *message) const { +void Syslog::log_(const int level, const char *tag, const char *message, size_t message_len) const { if (level > this->log_level_) return; // Syslog PRI calculation: facility * 8 + severity @@ -34,7 +36,7 @@ void Syslog::log_(const int level, const char *tag, const char *message) const { } int pri = this->facility_ * 8 + severity; auto timestamp = this->time_->now().strftime("%b %d %H:%M:%S"); - unsigned len = strlen(message); + size_t len = message_len; // remove color formatting if (this->strip_ && message[0] == 0x1B && len > 11) { message += 7; diff --git a/esphome/components/syslog/esphome_syslog.h b/esphome/components/syslog/esphome_syslog.h index 421a9bee73..e3b2f7dae5 100644 --- a/esphome/components/syslog/esphome_syslog.h +++ b/esphome/components/syslog/esphome_syslog.h @@ -17,7 +17,7 @@ class Syslog : public Component, public Parented { protected: int log_level_; - void log_(int level, const char *tag, const char *message) const; + void log_(int level, const char *tag, const char *message, size_t message_len) const; time::RealTimeClock *time_; bool strip_{true}; int facility_{16}; diff --git a/esphome/components/tca9555/__init__.py b/esphome/components/tca9555/__init__.py index db0451d4e6..f42e0fe398 100644 --- a/esphome/components/tca9555/__init__.py +++ b/esphome/components/tca9555/__init__.py @@ -53,7 +53,7 @@ TCA9555_PIN_SCHEMA = pins.gpio_base_schema( cv.int_range(min=0, max=15), modes=[CONF_INPUT, CONF_OUTPUT], mode_validator=validate_mode, - invertable=True, + invertible=True, ).extend( { cv.Required(CONF_TCA9555): cv.use_id(TCA9555Component), diff --git a/esphome/components/template/binary_sensor/template_binary_sensor.cpp b/esphome/components/template/binary_sensor/template_binary_sensor.cpp index 5ce8894a8a..d1fb618695 100644 --- a/esphome/components/template/binary_sensor/template_binary_sensor.cpp +++ b/esphome/components/template/binary_sensor/template_binary_sensor.cpp @@ -6,16 +6,8 @@ namespace template_ { static const char *const TAG = "template.binary_sensor"; -void TemplateBinarySensor::setup() { - if (!this->publish_initial_state_) - return; +void TemplateBinarySensor::setup() { this->loop(); } - if (this->f_ != nullptr) { - this->publish_initial_state(this->f_().value_or(false)); - } else { - this->publish_initial_state(false); - } -} void TemplateBinarySensor::loop() { if (this->f_ == nullptr) return; diff --git a/esphome/components/text/__init__.py b/esphome/components/text/__init__.py index 40b3a90d6b..8362e09ac0 100644 --- a/esphome/components/text/__init__.py +++ b/esphome/components/text/__init__.py @@ -14,8 +14,8 @@ from esphome.const import ( CONF_WEB_SERVER, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@mauritskorse"] IS_PLATFORM_COMPONENT = True @@ -58,6 +58,9 @@ _TEXT_SCHEMA = ( ) +_TEXT_SCHEMA.add_extra(entity_duplicate_validator("text")) + + def text_schema( class_: MockObjClass = cv.UNDEFINED, *, @@ -94,7 +97,7 @@ async def setup_text_core_( max_length: int | None, pattern: str | None, ): - await setup_entity(var, config) + await setup_entity(var, config, "text") cg.add(var.traits.set_min_length(min_length)) cg.add(var.traits.set_max_length(max_length)) diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index c7ac17c35a..abb2dcae6c 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -21,8 +21,8 @@ from esphome.const import ( DEVICE_CLASS_TIMESTAMP, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity from esphome.util import Registry DEVICE_CLASSES = [ @@ -153,6 +153,9 @@ _TEXT_SENSOR_SCHEMA = ( ) +_TEXT_SENSOR_SCHEMA.add_extra(entity_duplicate_validator("text_sensor")) + + def text_sensor_schema( class_: MockObjClass = cv.UNDEFINED, *, @@ -186,7 +189,7 @@ async def build_filters(config): async def setup_text_sensor_core_(var, config): - await setup_entity(var, config) + await setup_entity(var, config, "text_sensor") if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: cg.add(var.set_device_class(device_class)) diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index fe6ed8b159..404e585aff 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -997,7 +997,7 @@ void ThermostatClimate::change_preset_(climate::ClimatePreset preset) { auto config = this->preset_config_.find(preset); if (config != this->preset_config_.end()) { - ESP_LOGI(TAG, "Preset %s requested", LOG_STR_ARG(climate::climate_preset_to_string(preset))); + ESP_LOGV(TAG, "Preset %s requested", LOG_STR_ARG(climate::climate_preset_to_string(preset))); if (this->change_preset_internal_(config->second) || (!this->preset.has_value()) || this->preset.value() != preset) { // Fire any preset changed trigger if defined @@ -1015,7 +1015,7 @@ void ThermostatClimate::change_preset_(climate::ClimatePreset preset) { this->custom_preset.reset(); this->preset = preset; } else { - ESP_LOGE(TAG, "Preset %s is not configured, ignoring.", LOG_STR_ARG(climate::climate_preset_to_string(preset))); + ESP_LOGW(TAG, "Preset %s not configured; ignoring", LOG_STR_ARG(climate::climate_preset_to_string(preset))); } } @@ -1023,7 +1023,7 @@ void ThermostatClimate::change_custom_preset_(const std::string &custom_preset) auto config = this->custom_preset_config_.find(custom_preset); if (config != this->custom_preset_config_.end()) { - ESP_LOGI(TAG, "Custom preset %s requested", custom_preset.c_str()); + ESP_LOGV(TAG, "Custom preset %s requested", custom_preset.c_str()); if (this->change_preset_internal_(config->second) || (!this->custom_preset.has_value()) || this->custom_preset.value() != custom_preset) { // Fire any preset changed trigger if defined @@ -1041,7 +1041,7 @@ void ThermostatClimate::change_custom_preset_(const std::string &custom_preset) this->preset.reset(); this->custom_preset = custom_preset; } else { - ESP_LOGE(TAG, "Custom Preset %s is not configured, ignoring.", custom_preset.c_str()); + ESP_LOGW(TAG, "Custom preset %s not configured; ignoring", custom_preset.c_str()); } } @@ -1298,7 +1298,7 @@ void ThermostatClimate::dump_config() { if (this->supports_two_points_) { ESP_LOGCONFIG(TAG, " Minimum Set Point Differential: %.1f°C", this->set_point_minimum_differential_); } - ESP_LOGCONFIG(TAG, " Start-up Delay Enabled: %s", YESNO(this->use_startup_delay_)); + ESP_LOGCONFIG(TAG, " Use Start-up Delay: %s", YESNO(this->use_startup_delay_)); if (this->supports_cool_) { ESP_LOGCONFIG(TAG, " Cooling Parameters:\n" @@ -1353,44 +1353,47 @@ void ThermostatClimate::dump_config() { } ESP_LOGCONFIG(TAG, " Minimum Idle Time: %" PRIu32 "s", this->timer_[thermostat::TIMER_IDLE_ON].time / 1000); ESP_LOGCONFIG(TAG, - " Supports AUTO: %s\n" - " Supports HEAT/COOL: %s\n" - " Supports COOL: %s\n" - " Supports DRY: %s\n" - " Supports FAN_ONLY: %s\n" - " Supports FAN_ONLY_ACTION_USES_FAN_MODE_TIMER: %s\n" - " Supports FAN_ONLY_COOLING: %s", - YESNO(this->supports_auto_), YESNO(this->supports_heat_cool_), YESNO(this->supports_cool_), - YESNO(this->supports_dry_), YESNO(this->supports_fan_only_), + " Supported MODES:\n" + " AUTO: %s\n" + " HEAT/COOL: %s\n" + " HEAT: %s\n" + " COOL: %s\n" + " DRY: %s\n" + " FAN_ONLY: %s\n" + " FAN_ONLY_ACTION_USES_FAN_MODE_TIMER: %s\n" + " FAN_ONLY_COOLING: %s", + YESNO(this->supports_auto_), YESNO(this->supports_heat_cool_), YESNO(this->supports_heat_), + YESNO(this->supports_cool_), YESNO(this->supports_dry_), YESNO(this->supports_fan_only_), YESNO(this->supports_fan_only_action_uses_fan_mode_timer_), YESNO(this->supports_fan_only_cooling_)); if (this->supports_cool_) { - ESP_LOGCONFIG(TAG, " Supports FAN_WITH_COOLING: %s", YESNO(this->supports_fan_with_cooling_)); + ESP_LOGCONFIG(TAG, " FAN_WITH_COOLING: %s", YESNO(this->supports_fan_with_cooling_)); } if (this->supports_heat_) { - ESP_LOGCONFIG(TAG, " Supports FAN_WITH_HEATING: %s", YESNO(this->supports_fan_with_heating_)); + ESP_LOGCONFIG(TAG, " FAN_WITH_HEATING: %s", YESNO(this->supports_fan_with_heating_)); } - ESP_LOGCONFIG(TAG, " Supports HEAT: %s", YESNO(this->supports_heat_)); ESP_LOGCONFIG(TAG, - " Supports FAN MODE ON: %s\n" - " Supports FAN MODE OFF: %s\n" - " Supports FAN MODE AUTO: %s\n" - " Supports FAN MODE LOW: %s\n" - " Supports FAN MODE MEDIUM: %s\n" - " Supports FAN MODE HIGH: %s\n" - " Supports FAN MODE MIDDLE: %s\n" - " Supports FAN MODE FOCUS: %s\n" - " Supports FAN MODE DIFFUSE: %s\n" - " Supports FAN MODE QUIET: %s", + " Supported FAN MODES:\n" + " ON: %s\n" + " OFF: %s\n" + " AUTO: %s\n" + " LOW: %s\n" + " MEDIUM: %s\n" + " HIGH: %s\n" + " MIDDLE: %s\n" + " FOCUS: %s\n" + " DIFFUSE: %s\n" + " QUIET: %s", YESNO(this->supports_fan_mode_on_), YESNO(this->supports_fan_mode_off_), YESNO(this->supports_fan_mode_auto_), YESNO(this->supports_fan_mode_low_), YESNO(this->supports_fan_mode_medium_), YESNO(this->supports_fan_mode_high_), YESNO(this->supports_fan_mode_middle_), YESNO(this->supports_fan_mode_focus_), YESNO(this->supports_fan_mode_diffuse_), YESNO(this->supports_fan_mode_quiet_)); ESP_LOGCONFIG(TAG, - " Supports SWING MODE BOTH: %s\n" - " Supports SWING MODE OFF: %s\n" - " Supports SWING MODE HORIZONTAL: %s\n" - " Supports SWING MODE VERTICAL: %s\n" + " Supported SWING MODES:\n" + " BOTH: %s\n" + " OFF: %s\n" + " HORIZONTAL: %s\n" + " VERTICAL: %s\n" " Supports TWO SET POINTS: %s", YESNO(this->supports_swing_mode_both_), YESNO(this->supports_swing_mode_off_), YESNO(this->supports_swing_mode_horizontal_), YESNO(this->supports_swing_mode_vertical_), diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h index 50510cf070..007d7297d5 100644 --- a/esphome/components/thermostat/thermostat_climate.h +++ b/esphome/components/thermostat/thermostat_climate.h @@ -13,7 +13,7 @@ namespace esphome { namespace thermostat { -enum ThermostatClimateTimerIndex : size_t { +enum ThermostatClimateTimerIndex : uint8_t { TIMER_COOLING_MAX_RUN_TIME = 0, TIMER_COOLING_OFF = 1, TIMER_COOLING_ON = 2, @@ -26,7 +26,11 @@ enum ThermostatClimateTimerIndex : size_t { TIMER_IDLE_ON = 9, }; -enum OnBootRestoreFrom : size_t { MEMORY = 0, DEFAULT_PRESET = 1 }; +enum OnBootRestoreFrom : uint8_t { + MEMORY = 0, + DEFAULT_PRESET = 1, +}; + struct ThermostatClimateTimer { bool active; uint32_t time; @@ -65,7 +69,7 @@ class ThermostatClimate : public climate::Climate, public Component { void set_default_preset(const std::string &custom_preset); void set_default_preset(climate::ClimatePreset preset); - void set_on_boot_restore_from(thermostat::OnBootRestoreFrom on_boot_restore_from); + void set_on_boot_restore_from(OnBootRestoreFrom on_boot_restore_from); void set_set_point_minimum_differential(float differential); void set_cool_deadband(float deadband); void set_cool_overrun(float overrun); @@ -240,10 +244,8 @@ class ThermostatClimate : public climate::Climate, public Component { void dump_preset_config_(const char *preset_name, const ThermostatClimateTargetTempConfig &config, bool is_default_preset); - /// The sensor used for getting the current temperature - sensor::Sensor *sensor_{nullptr}; - /// The sensor used for getting the current humidity - sensor::Sensor *humidity_sensor_{nullptr}; + /// Minimum allowable duration in seconds for action timers + const uint8_t min_timer_duration_{1}; /// Whether the controller supports auto/cooling/drying/fanning/heating. /// @@ -310,6 +312,31 @@ class ThermostatClimate : public climate::Climate, public Component { /// setup_complete_ blocks modifying/resetting the temps immediately after boot bool setup_complete_{false}; + /// Store previously-known temperatures + /// + /// These are used to determine when the temperature change trigger/action needs to be called + float prev_target_temperature_{NAN}; + float prev_target_temperature_low_{NAN}; + float prev_target_temperature_high_{NAN}; + + /// Minimum differential required between set points + float set_point_minimum_differential_{0}; + + /// Hysteresis values used for computing climate actions + float cooling_deadband_{0}; + float cooling_overrun_{0}; + float heating_deadband_{0}; + float heating_overrun_{0}; + + /// Maximum allowable temperature deltas before engaging supplemental cooling/heating actions + float supplemental_cool_delta_{0}; + float supplemental_heat_delta_{0}; + + /// The sensor used for getting the current temperature + sensor::Sensor *sensor_{nullptr}; + /// The sensor used for getting the current humidity + sensor::Sensor *humidity_sensor_{nullptr}; + /// The trigger to call when the controller should switch to cooling action/mode. /// /// A null value for this attribute means that the controller has no cooling action @@ -399,7 +426,7 @@ class ThermostatClimate : public climate::Climate, public Component { /// The trigger to call when the target temperature(s) change(es). Trigger<> *temperature_change_trigger_{nullptr}; - /// The triggr to call when the preset mode changes + /// The trigger to call when the preset mode changes Trigger<> *preset_change_trigger_{nullptr}; /// A reference to the trigger that was previously active. @@ -411,6 +438,10 @@ class ThermostatClimate : public climate::Climate, public Component { Trigger<> *prev_mode_trigger_{nullptr}; Trigger<> *prev_swing_mode_trigger_{nullptr}; + /// If set to DEFAULT_PRESET then the default preset is always used. When MEMORY prior + /// state will attempt to be restored if possible + OnBootRestoreFrom on_boot_restore_from_{OnBootRestoreFrom::MEMORY}; + /// Store previously-known states /// /// These are used to determine when a trigger/action needs to be called @@ -419,28 +450,10 @@ class ThermostatClimate : public climate::Climate, public Component { climate::ClimateMode prev_mode_{climate::CLIMATE_MODE_OFF}; climate::ClimateSwingMode prev_swing_mode_{climate::CLIMATE_SWING_OFF}; - /// Store previously-known temperatures - /// - /// These are used to determine when the temperature change trigger/action needs to be called - float prev_target_temperature_{NAN}; - float prev_target_temperature_low_{NAN}; - float prev_target_temperature_high_{NAN}; - - /// Minimum differential required between set points - float set_point_minimum_differential_{0}; - - /// Hysteresis values used for computing climate actions - float cooling_deadband_{0}; - float cooling_overrun_{0}; - float heating_deadband_{0}; - float heating_overrun_{0}; - - /// Maximum allowable temperature deltas before engauging supplemental cooling/heating actions - float supplemental_cool_delta_{0}; - float supplemental_heat_delta_{0}; - - /// Minimum allowable duration in seconds for action timers - const uint8_t min_timer_duration_{1}; + /// Default standard preset to use on start up + climate::ClimatePreset default_preset_{}; + /// Default custom preset to use on start up + std::string default_custom_preset_{}; /// Climate action timers std::vector timer_{ @@ -460,15 +473,6 @@ class ThermostatClimate : public climate::Climate, public Component { std::map preset_config_{}; /// The set of custom preset configurations this thermostat supports (eg. "My Custom Preset") std::map custom_preset_config_{}; - - /// Default standard preset to use on start up - climate::ClimatePreset default_preset_{}; - /// Default custom preset to use on start up - std::string default_custom_preset_{}; - - /// If set to DEFAULT_PRESET then the default preset is always used. When MEMORY prior - /// state will attempt to be restored if possible - thermostat::OnBootRestoreFrom on_boot_restore_from_{thermostat::OnBootRestoreFrom::MEMORY}; }; } // namespace thermostat diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py index 6b3ff6f4d3..ab821d457b 100644 --- a/esphome/components/time/__init__.py +++ b/esphome/components/time/__init__.py @@ -268,7 +268,19 @@ def validate_tz(value: str) -> str: TIME_SCHEMA = cv.Schema( { - cv.Optional(CONF_TIMEZONE, default=detect_tz): validate_tz, + cv.SplitDefault( + CONF_TIMEZONE, + esp8266=detect_tz, + esp32=detect_tz, + rp2040=detect_tz, + bk72xx=detect_tz, + rtl87xx=detect_tz, + ln882x=detect_tz, + host=detect_tz, + ): cv.All( + cv.only_with_framework(["arduino", "esp-idf", "host"]), + validate_tz, + ), cv.Optional(CONF_ON_TIME): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CronTrigger), @@ -293,7 +305,9 @@ TIME_SCHEMA = cv.Schema( async def setup_time_core_(time_var, config): - cg.add(time_var.set_timezone(config[CONF_TIMEZONE])) + if timezone := config.get(CONF_TIMEZONE): + cg.add(time_var.set_timezone(timezone)) + cg.add_define("USE_TIME_TIMEZONE") for conf in config.get(CONF_ON_TIME, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], time_var) diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index 11e39e8f67..61391d2c6b 100644 --- a/esphome/components/time/real_time_clock.cpp +++ b/esphome/components/time/real_time_clock.cpp @@ -35,8 +35,10 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) { ret = settimeofday(&timev, nullptr); } +#ifdef USE_TIME_TIMEZONE // Move timezone back to local timezone. this->apply_timezone_(); +#endif if (ret != 0) { ESP_LOGW(TAG, "setimeofday() failed with code %d", ret); @@ -49,10 +51,12 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) { this->time_sync_callback_.call(); } +#ifdef USE_TIME_TIMEZONE void RealTimeClock::apply_timezone_() { setenv("TZ", this->timezone_.c_str(), 1); tzset(); } +#endif } // namespace time } // namespace esphome diff --git a/esphome/components/time/real_time_clock.h b/esphome/components/time/real_time_clock.h index 401798a568..4b98a88975 100644 --- a/esphome/components/time/real_time_clock.h +++ b/esphome/components/time/real_time_clock.h @@ -20,6 +20,7 @@ class RealTimeClock : public PollingComponent { public: explicit RealTimeClock(); +#ifdef USE_TIME_TIMEZONE /// Set the time zone. void set_timezone(const std::string &tz) { this->timezone_ = tz; @@ -28,6 +29,7 @@ class RealTimeClock : public PollingComponent { /// Get the time zone currently in use. std::string get_timezone() { return this->timezone_; } +#endif /// Get the time in the currently defined timezone. ESPTime now() { return ESPTime::from_epoch_local(this->timestamp_now()); } @@ -38,7 +40,7 @@ class RealTimeClock : public PollingComponent { /// Get the current time as the UTC epoch since January 1st 1970. time_t timestamp_now() { return ::time(nullptr); } - void add_on_time_sync_callback(std::function callback) { + void add_on_time_sync_callback(std::function &&callback) { this->time_sync_callback_.add(std::move(callback)); }; @@ -46,8 +48,10 @@ class RealTimeClock : public PollingComponent { /// Report a unix epoch as current time. void synchronize_epoch_(uint32_t epoch); +#ifdef USE_TIME_TIMEZONE std::string timezone_{}; void apply_timezone_(); +#endif CallbackManager time_sync_callback_; }; diff --git a/esphome/components/tlc5971/tlc5971.cpp b/esphome/components/tlc5971/tlc5971.cpp index ebcc3af361..05ff0a0080 100644 --- a/esphome/components/tlc5971/tlc5971.cpp +++ b/esphome/components/tlc5971/tlc5971.cpp @@ -24,8 +24,10 @@ void TLC5971::dump_config() { } void TLC5971::loop() { - if (!this->update_) + if (!this->update_) { + this->disable_loop(); return; + } uint32_t command; @@ -93,6 +95,7 @@ void TLC5971::set_channel_value(uint16_t channel, uint16_t value) { return; if (this->pwm_amounts_[channel] != value) { this->update_ = true; + this->enable_loop(); } this->pwm_amounts_[channel] = value; } diff --git a/esphome/components/tmp1075/tmp1075.h b/esphome/components/tmp1075/tmp1075.h index 84e2e8abe4..b5fd60c08e 100644 --- a/esphome/components/tmp1075/tmp1075.h +++ b/esphome/components/tmp1075/tmp1075.h @@ -58,8 +58,6 @@ class TMP1075Sensor : public PollingComponent, public sensor::Sensor, public i2c void setup() override; void update() override; - float get_setup_priority() const override { return setup_priority::DATA; } - void dump_config() override; // Call write_config() after calling any of these to send the new config to diff --git a/esphome/components/tof10120/tof10120_sensor.h b/esphome/components/tof10120/tof10120_sensor.h index 90bad8ed07..d0cca19d4c 100644 --- a/esphome/components/tof10120/tof10120_sensor.h +++ b/esphome/components/tof10120/tof10120_sensor.h @@ -12,7 +12,6 @@ class TOF10120Sensor : public sensor::Sensor, public PollingComponent, public i2 void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void update() override; }; } // namespace tof10120 diff --git a/esphome/components/tormatic/tormatic_cover.h b/esphome/components/tormatic/tormatic_cover.h index 33a2e1db8f..534d4bef14 100644 --- a/esphome/components/tormatic/tormatic_cover.h +++ b/esphome/components/tormatic/tormatic_cover.h @@ -16,7 +16,6 @@ class Tormatic : public cover::Cover, public uart::UARTDevice, public PollingCom void loop() override; void update() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; }; void set_open_duration(uint32_t duration) { this->open_duration_ = duration; } void set_close_duration(uint32_t duration) { this->close_duration_ = duration; } diff --git a/esphome/components/total_daily_energy/total_daily_energy.h b/esphome/components/total_daily_energy/total_daily_energy.h index 1a9d5d1a49..1145f54f95 100644 --- a/esphome/components/total_daily_energy/total_daily_energy.h +++ b/esphome/components/total_daily_energy/total_daily_energy.h @@ -23,7 +23,6 @@ class TotalDailyEnergy : public sensor::Sensor, public Component { void set_method(TotalDailyEnergyMethod method) { method_ = method; } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void loop() override; void publish_state_and_save(float state); diff --git a/esphome/components/tsl2591/tsl2591.cpp b/esphome/components/tsl2591/tsl2591.cpp index 1734d83dd2..c7622b116a 100644 --- a/esphome/components/tsl2591/tsl2591.cpp +++ b/esphome/components/tsl2591/tsl2591.cpp @@ -232,7 +232,7 @@ void TSL2591Component::set_integration_time_and_gain(TSL2591IntegrationTime inte this->integration_time_ = integration_time; this->gain_ = gain; if (!this->write_byte(TSL2591_COMMAND_BIT | TSL2591_REGISTER_CONTROL, - this->integration_time_ | this->gain_)) { // NOLINT + static_cast(this->integration_time_) | static_cast(this->gain_))) { ESP_LOGE(TAG, "I2C write failed"); } // The ADC values can be confused if gain or integration time are changed in the middle of a cycle. diff --git a/esphome/components/ttp229_bsf/ttp229_bsf.h b/esphome/components/ttp229_bsf/ttp229_bsf.h index 2663afcec9..fea4356b55 100644 --- a/esphome/components/ttp229_bsf/ttp229_bsf.h +++ b/esphome/components/ttp229_bsf/ttp229_bsf.h @@ -25,7 +25,6 @@ class TTP229BSFComponent : public Component { void register_channel(TTP229BSFChannel *channel) { this->channels_.push_back(channel); } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void loop() override { // check datavalid if sdo is high if (!this->sdo_pin_->digital_read()) { diff --git a/esphome/components/ttp229_lsf/ttp229_lsf.h b/esphome/components/ttp229_lsf/ttp229_lsf.h index f8775a17f0..7cc4bfca89 100644 --- a/esphome/components/ttp229_lsf/ttp229_lsf.h +++ b/esphome/components/ttp229_lsf/ttp229_lsf.h @@ -23,7 +23,6 @@ class TTP229LSFComponent : public Component, public i2c::I2CDevice { void register_channel(TTP229Channel *channel) { this->channels_.push_back(channel); } void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void loop() override; protected: diff --git a/esphome/components/tx20/tx20.cpp b/esphome/components/tx20/tx20.cpp index 73b865e8b8..42e3955fc2 100644 --- a/esphome/components/tx20/tx20.cpp +++ b/esphome/components/tx20/tx20.cpp @@ -152,7 +152,7 @@ void IRAM_ATTR Tx20ComponentStore::gpio_intr(Tx20ComponentStore *arg) { } arg->buffer[arg->buffer_index] = 1; arg->start_time = now; - arg->buffer_index++; + arg->buffer_index++; // NOLINT(clang-diagnostic-deprecated-volatile) return; } const uint32_t delay = now - arg->start_time; @@ -183,7 +183,7 @@ void IRAM_ATTR Tx20ComponentStore::gpio_intr(Tx20ComponentStore *arg) { } arg->spent_time += delay; arg->start_time = now; - arg->buffer_index++; + arg->buffer_index++; // NOLINT(clang-diagnostic-deprecated-volatile) } void IRAM_ATTR Tx20ComponentStore::reset() { tx20_available = false; diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index a0908a299c..7d4c6360fe 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -2,6 +2,7 @@ import re from esphome import automation, pins import esphome.codegen as cg +from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( CONF_AFTER, @@ -27,6 +28,7 @@ from esphome.const import ( CONF_TX_PIN, CONF_UART_ID, PLATFORM_HOST, + PlatformFramework, ) from esphome.core import CORE import esphome.final_validate as fv @@ -438,3 +440,19 @@ async def uart_write_to_code(config, action_id, template_arg, args): else: cg.add(var.set_data_static(data)) return var + + +FILTER_SOURCE_FILES = filter_source_files_from_platform( + { + "uart_component_esp32_arduino.cpp": {PlatformFramework.ESP32_ARDUINO}, + "uart_component_esp_idf.cpp": {PlatformFramework.ESP32_IDF}, + "uart_component_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, + "uart_component_host.cpp": {PlatformFramework.HOST_NATIVE}, + "uart_component_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO}, + "uart_component_libretiny.cpp": { + PlatformFramework.BK72XX_ARDUINO, + PlatformFramework.RTL87XX_ARDUINO, + PlatformFramework.LN882X_ARDUINO, + }, + } +) diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 8fae63a603..63b2579c3f 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -48,11 +48,7 @@ uart_config_t IDFUARTComponent::get_config_() { uart_config.parity = parity; uart_config.stop_bits = this->stop_bits_ == 1 ? UART_STOP_BITS_1 : UART_STOP_BITS_2; uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE; -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) uart_config.source_clk = UART_SCLK_DEFAULT; -#else - uart_config.source_clk = UART_SCLK_APB; -#endif uart_config.rx_flow_ctrl_thresh = 122; return uart_config; diff --git a/esphome/components/update/__init__.py b/esphome/components/update/__init__.py index 09b0698903..758267f412 100644 --- a/esphome/components/update/__init__.py +++ b/esphome/components/update/__init__.py @@ -15,8 +15,8 @@ from esphome.const import ( ENTITY_CATEGORY_CONFIG, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@jesserockz"] IS_PLATFORM_COMPONENT = True @@ -58,6 +58,9 @@ _UPDATE_SCHEMA = ( ) +_UPDATE_SCHEMA.add_extra(entity_duplicate_validator("update")) + + def update_schema( class_: MockObjClass = cv.UNDEFINED, *, @@ -87,7 +90,7 @@ UPDATE_SCHEMA.add_extra(cv.deprecated_schema_constant("update")) async def setup_update_core_(var, config): - await setup_entity(var, config) + await setup_entity(var, config, "update") if device_class_config := config.get(CONF_DEVICE_CLASS): cg.add(var.set_device_class(device_class_config)) diff --git a/esphome/components/update/update_entity.h b/esphome/components/update/update_entity.h index 169e580457..9424e80b9f 100644 --- a/esphome/components/update/update_entity.h +++ b/esphome/components/update/update_entity.h @@ -1,5 +1,6 @@ #pragma once +#include #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/entity_base.h" @@ -38,12 +39,19 @@ class UpdateEntity : public EntityBase, public EntityBase_DeviceClass { const UpdateState &state = state_; void add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } + Trigger *get_update_available_trigger() { + if (!update_available_trigger_) { + update_available_trigger_ = std::make_unique>(); + } + return update_available_trigger_.get(); + } protected: UpdateState state_{UPDATE_STATE_UNKNOWN}; UpdateInfo update_info_; CallbackManager state_callback_{}; + std::unique_ptr> update_available_trigger_{nullptr}; }; } // namespace update diff --git a/esphome/components/usb_host/__init__.py b/esphome/components/usb_host/__init__.py index b6ca779706..0fe3310127 100644 --- a/esphome/components/usb_host/__init__.py +++ b/esphome/components/usb_host/__init__.py @@ -6,7 +6,7 @@ from esphome.components.esp32 import ( only_on_variant, ) import esphome.config_validation as cv -from esphome.const import CONF_ID +from esphome.const import CONF_DEVICES, CONF_ID from esphome.cpp_types import Component AUTO_LOAD = ["bytebuffer"] @@ -16,9 +16,9 @@ usb_host_ns = cg.esphome_ns.namespace("usb_host") USBHost = usb_host_ns.class_("USBHost", Component) USBClient = usb_host_ns.class_("USBClient", Component) -CONF_DEVICES = "devices" CONF_VID = "vid" CONF_PID = "pid" +CONF_ENABLE_HUBS = "enable_hubs" def usb_device_schema(cls=USBClient, vid: int = None, pid: [int] = None) -> cv.Schema: @@ -42,6 +42,7 @@ CONFIG_SCHEMA = cv.All( cv.COMPONENT_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(USBHost), + cv.Optional(CONF_ENABLE_HUBS, default=False): cv.boolean, cv.Optional(CONF_DEVICES): cv.ensure_list(usb_device_schema()), } ), @@ -58,6 +59,8 @@ async def register_usb_client(config): async def to_code(config): add_idf_sdkconfig_option("CONFIG_USB_HOST_CONTROL_TRANSFER_MAX_SIZE", 1024) + if config.get(CONF_ENABLE_HUBS): + add_idf_sdkconfig_option("CONFIG_USB_HOST_HUBS_SUPPORTED", True) var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) for device in config.get(CONF_DEVICES) or (): diff --git a/esphome/components/valve/__init__.py b/esphome/components/valve/__init__.py index a6f1428cd2..cb27546120 100644 --- a/esphome/components/valve/__init__.py +++ b/esphome/components/valve/__init__.py @@ -22,8 +22,8 @@ from esphome.const import ( DEVICE_CLASS_WATER, ) from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity IS_PLATFORM_COMPONENT = True @@ -103,6 +103,9 @@ _VALVE_SCHEMA = ( ) +_VALVE_SCHEMA.add_extra(entity_duplicate_validator("valve")) + + def valve_schema( class_: MockObjClass = cv.UNDEFINED, *, @@ -132,7 +135,7 @@ VALVE_SCHEMA.add_extra(cv.deprecated_schema_constant("valve")) async def _setup_valve_core(var, config): - await setup_entity(var, config) + await setup_entity(var, config, "valve") if device_class_config := config.get(CONF_DEVICE_CLASS): cg.add(var.set_device_class(device_class_config)) diff --git a/esphome/components/vbus/vbus.h b/esphome/components/vbus/vbus.h index 7e97b5049a..0a253f1bdb 100644 --- a/esphome/components/vbus/vbus.h +++ b/esphome/components/vbus/vbus.h @@ -30,7 +30,6 @@ class VBus : public uart::UARTDevice, public Component { public: void dump_config() override; void loop() override; - float get_setup_priority() const override { return setup_priority::DATA; } void register_listener(VBusListener *listener) { this->listeners_.push_back(listener); } diff --git a/esphome/components/veml3235/veml3235.h b/esphome/components/veml3235/veml3235.h index 2b0d6b23ea..b57e1571f1 100644 --- a/esphome/components/veml3235/veml3235.h +++ b/esphome/components/veml3235/veml3235.h @@ -65,7 +65,6 @@ class VEML3235Sensor : public sensor::Sensor, public PollingComponent, public i2 void setup() override; void dump_config() override; void update() override { this->publish_state(this->read_lx_()); } - float get_setup_priority() const override { return setup_priority::DATA; } // Used by ESPHome framework. Does NOT actually set the value on the device. void set_auto_gain(bool auto_gain) { this->auto_gain_ = auto_gain; } diff --git a/esphome/components/veml7700/veml7700.h b/esphome/components/veml7700/veml7700.h index 17fee6b851..b0d1451cf0 100644 --- a/esphome/components/veml7700/veml7700.h +++ b/esphome/components/veml7700/veml7700.h @@ -102,7 +102,6 @@ class VEML7700Component : public PollingComponent, public i2c::I2CDevice { // // EspHome framework functions // - float get_setup_priority() const override { return setup_priority::DATA; } void setup() override; void dump_config() override; void update() override; diff --git a/esphome/components/vl53l0x/vl53l0x_sensor.h b/esphome/components/vl53l0x/vl53l0x_sensor.h index dd76e8e0ab..2bf90015fe 100644 --- a/esphome/components/vl53l0x/vl53l0x_sensor.h +++ b/esphome/components/vl53l0x/vl53l0x_sensor.h @@ -30,7 +30,6 @@ class VL53L0XSensor : public sensor::Sensor, public PollingComponent, public i2c void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void update() override; void loop() override; diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 879d9492f0..9cf7d10936 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -85,7 +85,7 @@ bool VoiceAssistant::start_udp_socket_() { bool VoiceAssistant::allocate_buffers_() { #ifdef USE_SPEAKER if ((this->speaker_ != nullptr) && (this->speaker_buffer_ == nullptr)) { - ExternalRAMAllocator speaker_allocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator speaker_allocator; this->speaker_buffer_ = speaker_allocator.allocate(SPEAKER_BUFFER_SIZE); if (this->speaker_buffer_ == nullptr) { ESP_LOGW(TAG, "Could not allocate speaker buffer"); @@ -103,7 +103,7 @@ bool VoiceAssistant::allocate_buffers_() { } if (this->send_buffer_ == nullptr) { - ExternalRAMAllocator send_allocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator send_allocator; this->send_buffer_ = send_allocator.allocate(SEND_BUFFER_SIZE); if (send_buffer_ == nullptr) { ESP_LOGW(TAG, "Could not allocate send buffer"); @@ -136,7 +136,7 @@ void VoiceAssistant::clear_buffers_() { void VoiceAssistant::deallocate_buffers_() { if (this->send_buffer_ != nullptr) { - ExternalRAMAllocator send_deallocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator send_deallocator; send_deallocator.deallocate(this->send_buffer_, SEND_BUFFER_SIZE); this->send_buffer_ = nullptr; } @@ -147,7 +147,7 @@ void VoiceAssistant::deallocate_buffers_() { #ifdef USE_SPEAKER if ((this->speaker_ != nullptr) && (this->speaker_buffer_ != nullptr)) { - ExternalRAMAllocator speaker_deallocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator speaker_deallocator; speaker_deallocator.deallocate(this->speaker_buffer_, SPEAKER_BUFFER_SIZE); this->speaker_buffer_ = nullptr; } diff --git a/esphome/components/watchdog/watchdog.cpp b/esphome/components/watchdog/watchdog.cpp index f6f2992a11..2ce46756e4 100644 --- a/esphome/components/watchdog/watchdog.cpp +++ b/esphome/components/watchdog/watchdog.cpp @@ -38,16 +38,12 @@ WatchdogManager::~WatchdogManager() { void WatchdogManager::set_timeout_(uint32_t timeout_ms) { ESP_LOGV(TAG, "Adjusting WDT to %" PRIu32 "ms", timeout_ms); #ifdef USE_ESP32 -#if ESP_IDF_VERSION_MAJOR >= 5 esp_task_wdt_config_t wdt_config = { .timeout_ms = timeout_ms, .idle_core_mask = (1 << SOC_CPU_CORES_NUM) - 1, .trigger_panic = true, }; esp_task_wdt_reconfigure(&wdt_config); -#else - esp_task_wdt_init(timeout_ms / 1000, true); -#endif // ESP_IDF_VERSION_MAJOR #endif // USE_ESP32 #ifdef USE_RP2040 diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index 084747c09e..75c6b84b79 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -1,9 +1,9 @@ #include "waveshare_epaper.h" +#include +#include #include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include -#include namespace esphome { namespace waveshare_epaper { @@ -185,7 +185,7 @@ void WaveshareEPaper7C::setup() { this->initialize(); } void WaveshareEPaper7C::init_internal_7c_(uint32_t buffer_length) { - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator allocator; uint32_t small_buffer_length = buffer_length / NUM_BUFFERS; for (int i = 0; i < NUM_BUFFERS; i++) { @@ -2054,7 +2054,7 @@ void GDEW029T5::initialize() { this->deep_sleep_between_updates_ = true; // old buffer for partial update - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator allocator; this->old_buffer_ = allocator.allocate(this->get_buffer_length_()); if (this->old_buffer_ == nullptr) { ESP_LOGE(TAG, "Could not allocate old buffer for display!"); @@ -2199,7 +2199,7 @@ void GDEW029T5::dump_config() { void GDEW0154M09::initialize() { this->init_internal_(); - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + RAMAllocator allocator; this->lastbuff_ = allocator.allocate(this->get_buffer_length_()); if (this->lastbuff_ != nullptr) { memset(this->lastbuff_, 0xff, sizeof(uint8_t) * this->get_buffer_length_()); @@ -3132,7 +3132,7 @@ void HOT GDEY0583T81::display() { } else { // Partial out (PTOUT), makes the display exit partial mode this->command(0x92); - ESP_LOGD(TAG, "Partial update done, next full update after %d cycles", + ESP_LOGD(TAG, "Partial update done, next full update after %" PRIu32 " cycles", this->full_update_every_ - this->at_update_ - 1); } diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index d846a3418b..6890f60014 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -28,10 +28,12 @@ from esphome.const import ( PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, + PLATFORM_LN882X, PLATFORM_RTL87XX, ) from esphome.core import CORE, coroutine_with_priority import esphome.final_validate as fv +from esphome.types import ConfigType AUTO_LOAD = ["json", "web_server_base"] @@ -39,13 +41,14 @@ CONF_SORTING_GROUP_ID = "sorting_group_id" CONF_SORTING_GROUPS = "sorting_groups" CONF_SORTING_WEIGHT = "sorting_weight" + web_server_ns = cg.esphome_ns.namespace("web_server") WebServer = web_server_ns.class_("WebServer", cg.Component, cg.Controller) sorting_groups = {} -def default_url(config): +def default_url(config: ConfigType) -> ConfigType: config = config.copy() if config[CONF_VERSION] == 1: if CONF_CSS_URL not in config: @@ -65,19 +68,27 @@ def default_url(config): return config -def validate_local(config): +def validate_local(config: ConfigType) -> ConfigType: if CONF_LOCAL in config and config[CONF_VERSION] == 1: raise cv.Invalid("'local' is not supported in version 1") return config -def validate_ota(config): - if CORE.using_esp_idf and config[CONF_OTA]: - raise cv.Invalid("Enabling 'ota' is not supported for IDF framework yet") +def validate_ota_removed(config: ConfigType) -> ConfigType: + # Only raise error if OTA is explicitly enabled (True) + # If it's False or not specified, we can safely ignore it + if config.get(CONF_OTA): + raise cv.Invalid( + f"The '{CONF_OTA}' option has been removed from 'web_server'. " + f"Please use the new OTA platform structure instead:\n\n" + f"ota:\n" + f" - platform: web_server\n\n" + f"See https://esphome.io/components/ota for more information." + ) return config -def validate_sorting_groups(config): +def validate_sorting_groups(config: ConfigType) -> ConfigType: if CONF_SORTING_GROUPS in config and config[CONF_VERSION] != 3: raise cv.Invalid( f"'{CONF_SORTING_GROUPS}' is only supported in 'web_server' version 3" @@ -88,7 +99,7 @@ def validate_sorting_groups(config): def _validate_no_sorting_component( sorting_component: str, webserver_version: int, - config: dict, + config: ConfigType, path: list[str] | None = None, ) -> None: if path is None: @@ -111,7 +122,7 @@ def _validate_no_sorting_component( ) -def _final_validate_sorting(config): +def _final_validate_sorting(config: ConfigType) -> ConfigType: if (webserver_version := config.get(CONF_VERSION)) != 3: _validate_no_sorting_component( CONF_SORTING_WEIGHT, webserver_version, fv.full_config.get() @@ -174,24 +185,25 @@ CONFIG_SCHEMA = cv.All( web_server_base.WebServerBase ), cv.Optional(CONF_INCLUDE_INTERNAL, default=False): cv.boolean, - cv.SplitDefault( - CONF_OTA, - esp8266=True, - esp32_arduino=True, - esp32_idf=False, - bk72xx=True, - rtl87xx=True, - ): cv.boolean, + cv.Optional(CONF_OTA, default=False): cv.boolean, cv.Optional(CONF_LOG, default=True): cv.boolean, cv.Optional(CONF_LOCAL): cv.boolean, cv.Optional(CONF_SORTING_GROUPS): cv.ensure_list(sorting_group), } ).extend(cv.COMPONENT_SCHEMA), - cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX, PLATFORM_RTL87XX]), + cv.only_on( + [ + PLATFORM_ESP32, + PLATFORM_ESP8266, + PLATFORM_BK72XX, + PLATFORM_LN882X, + PLATFORM_RTL87XX, + ] + ), default_url, validate_local, - validate_ota, validate_sorting_groups, + validate_ota_removed, ) @@ -211,6 +223,7 @@ async def add_entity_config(entity, config): sorting_weight = config.get(CONF_SORTING_WEIGHT, 50) sorting_group_hash = hash(config.get(CONF_SORTING_GROUP_ID)) + cg.add_define("USE_WEBSERVER_SORTING") cg.add( web_server.add_entity_config( entity, @@ -274,7 +287,8 @@ async def to_code(config): else: cg.add(var.set_css_url(config[CONF_CSS_URL])) cg.add(var.set_js_url(config[CONF_JS_URL])) - cg.add(var.set_allow_ota(config[CONF_OTA])) + # OTA is now handled by the web_server OTA platform + # The CONF_OTA option is kept only for backwards compatibility validation cg.add(var.set_expose_log(config[CONF_LOG])) if config[CONF_ENABLE_PRIVATE_NETWORK_ACCESS]: cg.add_define("USE_WEBSERVER_PRIVATE_NETWORK_ACCESS") @@ -296,4 +310,5 @@ async def to_code(config): cg.add_define("USE_WEBSERVER_LOCAL") if (sorting_group_config := config.get(CONF_SORTING_GROUPS)) is not None: + cg.add_define("USE_WEBSERVER_SORTING") add_sorting_groups(var, sorting_group_config) diff --git a/esphome/components/web_server/ota/__init__.py b/esphome/components/web_server/ota/__init__.py new file mode 100644 index 0000000000..3af14fd453 --- /dev/null +++ b/esphome/components/web_server/ota/__init__.py @@ -0,0 +1,32 @@ +import esphome.codegen as cg +from esphome.components.esp32 import add_idf_component +from esphome.components.ota import BASE_OTA_SCHEMA, OTAComponent, ota_to_code +import esphome.config_validation as cv +from esphome.const import CONF_ID +from esphome.core import CORE, coroutine_with_priority + +CODEOWNERS = ["@esphome/core"] +DEPENDENCIES = ["network", "web_server_base"] + +web_server_ns = cg.esphome_ns.namespace("web_server") +WebServerOTAComponent = web_server_ns.class_("WebServerOTAComponent", OTAComponent) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(WebServerOTAComponent), + } + ) + .extend(BASE_OTA_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + + +@coroutine_with_priority(52.0) +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await ota_to_code(var, config) + await cg.register_component(var, config) + cg.add_define("USE_WEBSERVER_OTA") + if CORE.using_esp_idf: + add_idf_component(name="zorxx/multipart-parser", ref="1.0.1") diff --git a/esphome/components/web_server/ota/ota_web_server.cpp b/esphome/components/web_server/ota/ota_web_server.cpp new file mode 100644 index 0000000000..4f8f6fda17 --- /dev/null +++ b/esphome/components/web_server/ota/ota_web_server.cpp @@ -0,0 +1,210 @@ +#include "ota_web_server.h" +#ifdef USE_WEBSERVER_OTA + +#include "esphome/components/ota/ota_backend.h" +#include "esphome/core/application.h" +#include "esphome/core/log.h" + +#ifdef USE_ARDUINO +#ifdef USE_ESP8266 +#include +#elif defined(USE_ESP32) || defined(USE_LIBRETINY) +#include +#endif +#endif // USE_ARDUINO + +namespace esphome { +namespace web_server { + +static const char *const TAG = "web_server.ota"; + +class OTARequestHandler : public AsyncWebHandler { + public: + OTARequestHandler(WebServerOTAComponent *parent) : parent_(parent) {} + void handleRequest(AsyncWebServerRequest *request) override; + void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, + bool final) override; + bool canHandle(AsyncWebServerRequest *request) const override { + return request->url() == "/update" && request->method() == HTTP_POST; + } + + // NOLINTNEXTLINE(readability-identifier-naming) + bool isRequestHandlerTrivial() const override { return false; } + + protected: + void report_ota_progress_(AsyncWebServerRequest *request); + void schedule_ota_reboot_(); + void ota_init_(const char *filename); + + uint32_t last_ota_progress_{0}; + uint32_t ota_read_length_{0}; + WebServerOTAComponent *parent_; + bool ota_success_{false}; + + private: + std::unique_ptr ota_backend_{nullptr}; +}; + +void OTARequestHandler::report_ota_progress_(AsyncWebServerRequest *request) { + const uint32_t now = millis(); + if (now - this->last_ota_progress_ > 1000) { + float percentage = 0.0f; + if (request->contentLength() != 0) { + // Note: Using contentLength() for progress calculation is technically wrong as it includes + // multipart headers/boundaries, but it's only off by a small amount and we don't have + // access to the actual firmware size until the upload is complete. This is intentional + // as it still gives the user a reasonable progress indication. + percentage = (this->ota_read_length_ * 100.0f) / request->contentLength(); + ESP_LOGD(TAG, "OTA in progress: %0.1f%%", percentage); + } else { + ESP_LOGD(TAG, "OTA in progress: %u bytes read", this->ota_read_length_); + } +#ifdef USE_OTA_STATE_CALLBACK + // Report progress - use call_deferred since we're in web server task + this->parent_->state_callback_.call_deferred(ota::OTA_IN_PROGRESS, percentage, 0); +#endif + this->last_ota_progress_ = now; + } +} + +void OTARequestHandler::schedule_ota_reboot_() { + ESP_LOGI(TAG, "OTA update successful!"); + this->parent_->set_timeout(100, []() { + ESP_LOGI(TAG, "Performing OTA reboot now"); + App.safe_reboot(); + }); +} + +void OTARequestHandler::ota_init_(const char *filename) { + ESP_LOGI(TAG, "OTA Update Start: %s", filename); + this->ota_read_length_ = 0; + this->ota_success_ = false; +} + +void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, + uint8_t *data, size_t len, bool final) { + ota::OTAResponseTypes error_code = ota::OTA_RESPONSE_OK; + + if (index == 0 && !this->ota_backend_) { + // Initialize OTA on first call + this->ota_init_(filename.c_str()); + +#ifdef USE_OTA_STATE_CALLBACK + // Notify OTA started - use call_deferred since we're in web server task + this->parent_->state_callback_.call_deferred(ota::OTA_STARTED, 0.0f, 0); +#endif + + // Platform-specific pre-initialization +#ifdef USE_ARDUINO +#ifdef USE_ESP8266 + Update.runAsync(true); +#endif +#if defined(USE_ESP32) || defined(USE_LIBRETINY) + if (Update.isRunning()) { + Update.abort(); + } +#endif +#endif // USE_ARDUINO + + this->ota_backend_ = ota::make_ota_backend(); + if (!this->ota_backend_) { + ESP_LOGE(TAG, "Failed to create OTA backend"); +#ifdef USE_OTA_STATE_CALLBACK + this->parent_->state_callback_.call_deferred(ota::OTA_ERROR, 0.0f, + static_cast(ota::OTA_RESPONSE_ERROR_UNKNOWN)); +#endif + return; + } + + // Web server OTA uses multipart uploads where the actual firmware size + // is unknown (contentLength includes multipart overhead) + // Pass 0 to indicate unknown size + error_code = this->ota_backend_->begin(0); + if (error_code != ota::OTA_RESPONSE_OK) { + ESP_LOGE(TAG, "OTA begin failed: %d", error_code); + this->ota_backend_.reset(); +#ifdef USE_OTA_STATE_CALLBACK + this->parent_->state_callback_.call_deferred(ota::OTA_ERROR, 0.0f, static_cast(error_code)); +#endif + return; + } + } + + if (!this->ota_backend_) { + return; + } + + // Process data + if (len > 0) { + error_code = this->ota_backend_->write(data, len); + if (error_code != ota::OTA_RESPONSE_OK) { + ESP_LOGE(TAG, "OTA write failed: %d", error_code); + this->ota_backend_->abort(); + this->ota_backend_.reset(); +#ifdef USE_OTA_STATE_CALLBACK + this->parent_->state_callback_.call_deferred(ota::OTA_ERROR, 0.0f, static_cast(error_code)); +#endif + return; + } + this->ota_read_length_ += len; + this->report_ota_progress_(request); + } + + // Finalize + if (final) { + ESP_LOGD(TAG, "OTA final chunk: index=%u, len=%u, total_read=%u, contentLength=%u", index, len, + this->ota_read_length_, request->contentLength()); + + // For Arduino framework, the Update library tracks expected size from firmware header + // If we haven't received enough data, calling end() will fail + // This can happen if the upload is interrupted or the client disconnects + error_code = this->ota_backend_->end(); + if (error_code == ota::OTA_RESPONSE_OK) { + this->ota_success_ = true; +#ifdef USE_OTA_STATE_CALLBACK + // Report completion before reboot - use call_deferred since we're in web server task + this->parent_->state_callback_.call_deferred(ota::OTA_COMPLETED, 100.0f, 0); +#endif + this->schedule_ota_reboot_(); + } else { + ESP_LOGE(TAG, "OTA end failed: %d", error_code); +#ifdef USE_OTA_STATE_CALLBACK + this->parent_->state_callback_.call_deferred(ota::OTA_ERROR, 0.0f, static_cast(error_code)); +#endif + } + this->ota_backend_.reset(); + } +} + +void OTARequestHandler::handleRequest(AsyncWebServerRequest *request) { + AsyncWebServerResponse *response; + // Use the ota_success_ flag to determine the actual result + const char *msg = this->ota_success_ ? "Update Successful!" : "Update Failed!"; + response = request->beginResponse(200, "text/plain", msg); + response->addHeader("Connection", "close"); + request->send(response); +} + +void WebServerOTAComponent::setup() { + // Get the global web server base instance and register our handler + auto *base = web_server_base::global_web_server_base; + if (base == nullptr) { + ESP_LOGE(TAG, "WebServerBase not found"); + this->mark_failed(); + return; + } + + // AsyncWebServer takes ownership of the handler and will delete it when the server is destroyed + base->add_handler(new OTARequestHandler(this)); // NOLINT +#ifdef USE_OTA_STATE_CALLBACK + // Register with global OTA callback system + ota::register_ota_platform(this); +#endif +} + +void WebServerOTAComponent::dump_config() { ESP_LOGCONFIG(TAG, "Web Server OTA"); } + +} // namespace web_server +} // namespace esphome + +#endif // USE_WEBSERVER_OTA diff --git a/esphome/components/web_server/ota/ota_web_server.h b/esphome/components/web_server/ota/ota_web_server.h new file mode 100644 index 0000000000..a7170c0e34 --- /dev/null +++ b/esphome/components/web_server/ota/ota_web_server.h @@ -0,0 +1,26 @@ +#pragma once + +#include "esphome/core/defines.h" +#ifdef USE_WEBSERVER_OTA + +#include "esphome/components/ota/ota_backend.h" +#include "esphome/components/web_server_base/web_server_base.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace web_server { + +class WebServerOTAComponent : public ota::OTAComponent { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + + protected: + friend class OTARequestHandler; +}; + +} // namespace web_server +} // namespace esphome + +#endif // USE_WEBSERVER_OTA diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 82e2442c9f..8ced5b7e18 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -46,29 +46,60 @@ static const char *const HEADER_CORS_REQ_PNA = "Access-Control-Request-Private-N static const char *const HEADER_CORS_ALLOW_PNA = "Access-Control-Allow-Private-Network"; #endif -UrlMatch match_url(const std::string &url, bool only_domain = false) { - UrlMatch match; - match.valid = false; - size_t domain_end = url.find('/', 1); - if (domain_end == std::string::npos) - return match; - match.domain = url.substr(1, domain_end - 1); - if (only_domain) { - match.valid = true; +// Parse URL and return match info +static UrlMatch match_url(const char *url_ptr, size_t url_len, bool only_domain) { + UrlMatch match{}; + + // URL must start with '/' + if (url_len < 2 || url_ptr[0] != '/') { return match; } - if (url.length() == domain_end - 1) + + // Skip leading '/' + const char *start = url_ptr + 1; + const char *end = url_ptr + url_len; + + // Find domain (everything up to next '/' or end) + const char *domain_end = (const char *) memchr(start, '/', end - start); + if (!domain_end) { + // No second slash found - original behavior returns invalid return match; - size_t id_begin = domain_end + 1; - size_t id_end = url.find('/', id_begin); + } + + // Set domain + match.domain = start; + match.domain_len = domain_end - start; match.valid = true; - if (id_end == std::string::npos) { - match.id = url.substr(id_begin, url.length() - id_begin); + + if (only_domain) { return match; } - match.id = url.substr(id_begin, id_end - id_begin); - size_t method_begin = id_end + 1; - match.method = url.substr(method_begin, url.length() - method_begin); + + // Parse ID if present + if (domain_end + 1 >= end) { + return match; // Nothing after domain slash + } + + const char *id_start = domain_end + 1; + const char *id_end = (const char *) memchr(id_start, '/', end - id_start); + + if (!id_end) { + // No more slashes, entire remaining string is ID + match.id = id_start; + match.id_len = end - id_start; + return match; + } + + // Set ID + match.id = id_start; + match.id_len = id_end - id_start; + + // Parse method if present + if (id_end + 1 < end) { + match.method = id_end + 1; + match.method_len = end - (id_end + 1); + } + return match; } @@ -91,10 +122,19 @@ void DeferredUpdateEventSource::process_deferred_queue_() { while (!deferred_queue_.empty()) { DeferredEvent &de = deferred_queue_.front(); std::string message = de.message_generator_(web_server_, de.source_); - if (this->try_send(message.c_str(), "state")) { + if (this->send(message.c_str(), "state") != DISCARDED) { // O(n) but memory efficiency is more important than speed here which is why std::vector was chosen deferred_queue_.erase(deferred_queue_.begin()); + this->consecutive_send_failures_ = 0; // Reset failure count on successful send } else { + this->consecutive_send_failures_++; + if (this->consecutive_send_failures_ >= MAX_CONSECUTIVE_SEND_FAILURES) { + // Too many failures, connection is likely dead + ESP_LOGW(TAG, "Closing stuck EventSource connection after %" PRIu16 " failed sends", + this->consecutive_send_failures_); + this->close(); + this->deferred_queue_.clear(); + } break; } } @@ -131,8 +171,10 @@ void DeferredUpdateEventSource::deferrable_send_state(void *source, const char * deq_push_back_with_dedup_(source, message_generator); } else { std::string message = message_generator(web_server_, source); - if (!this->try_send(message.c_str(), "state")) { + if (this->send(message.c_str(), "state") == DISCARDED) { deq_push_back_with_dedup_(source, message_generator); + } else { + this->consecutive_send_failures_ = 0; // Reset failure count on successful send } } } @@ -171,8 +213,8 @@ void DeferredUpdateEventSourceList::add_new_client(WebServer *ws, AsyncWebServer ws->defer([this, ws, es]() { this->on_client_connect_(ws, es); }); }); - es->onDisconnect([this, ws](AsyncEventSource *source, AsyncEventSourceClient *client) { - ws->defer([this, source]() { this->on_client_disconnect_((DeferredUpdateEventSource *) source); }); + es->onDisconnect([this, ws, es](AsyncEventSourceClient *client) { + ws->defer([this, es]() { this->on_client_disconnect_((DeferredUpdateEventSource *) es); }); }); es->handleRequest(request); @@ -184,6 +226,7 @@ void DeferredUpdateEventSourceList::on_client_connect_(WebServer *ws, DeferredUp std::string message = ws->get_config_json(); source->try_send_nodefer(message.c_str(), "ping", millis(), 30000); +#ifdef USE_WEBSERVER_SORTING for (auto &group : ws->sorting_groups_) { message = json::build_json([group](JsonObject root) { root["name"] = group.second.name; @@ -193,6 +236,7 @@ void DeferredUpdateEventSourceList::on_client_connect_(WebServer *ws, DeferredUp // up to 31 groups should be able to be queued initially without defer source->try_send_nodefer(message.c_str(), "sorting_group"); } +#endif source->entities_iterator_.begin(ws->include_internal_); @@ -211,11 +255,7 @@ void DeferredUpdateEventSourceList::on_client_disconnect_(DeferredUpdateEventSou } #endif -WebServer::WebServer(web_server_base::WebServerBase *base) : base_(base) { -#ifdef USE_ESP32 - to_schedule_lock_ = xSemaphoreCreateMutex(); -#endif -} +WebServer::WebServer(web_server_base::WebServerBase *base) : base_(base) {} #ifdef USE_WEBSERVER_CSS_INCLUDE void WebServer::set_css_include(const char *css_include) { this->css_include_ = css_include; } @@ -228,7 +268,11 @@ std::string WebServer::get_config_json() { return json::build_json([this](JsonObject root) { root["title"] = App.get_friendly_name().empty() ? App.get_name() : App.get_friendly_name(); root["comment"] = App.get_comment(); - root["ota"] = this->allow_ota_; +#ifdef USE_WEBSERVER_OTA + root["ota"] = true; // web_server OTA platform is configured +#else + root["ota"] = false; +#endif root["log"] = this->expose_log_; root["lang"] = "en"; }); @@ -243,7 +287,8 @@ void WebServer::setup() { if (logger::global_logger != nullptr && this->expose_log_) { logger::global_logger->add_on_log_callback( // logs are not deferred, the memory overhead would be too large - [this](int level, const char *tag, const char *message) { + [this](int level, const char *tag, const char *message, size_t message_len) { + (void) message_len; this->events_.try_send_nodefer(message, "log", millis()); }); } @@ -254,33 +299,13 @@ void WebServer::setup() { #endif this->base_->add_handler(this); - if (this->allow_ota_) - this->base_->add_ota_handler(); + // OTA is now handled by the web_server OTA platform // doesn't need defer functionality - if the queue is full, the client JS knows it's alive because it's clearly // getting a lot of events this->set_interval(10000, [this]() { this->events_.try_send_nodefer("", "ping", millis(), 30000); }); } -void WebServer::loop() { -#ifdef USE_ESP32 - if (xSemaphoreTake(this->to_schedule_lock_, 0L)) { - std::function fn; - if (!to_schedule_.empty()) { - // scheduler execute things out of order which may lead to incorrect state - // this->defer(std::move(to_schedule_.front())); - // let's execute it directly from the loop - fn = std::move(to_schedule_.front()); - to_schedule_.pop_front(); - } - xSemaphoreGive(this->to_schedule_lock_); - if (fn) { - fn(); - } - } -#endif - - this->events_.loop(); -} +void WebServer::loop() { this->events_.loop(); } void WebServer::dump_config() { ESP_LOGCONFIG(TAG, "Web Server:\n" @@ -291,14 +316,23 @@ float WebServer::get_setup_priority() const { return setup_priority::WIFI - 1.0f #ifdef USE_WEBSERVER_LOCAL void WebServer::handle_index_request(AsyncWebServerRequest *request) { +#ifndef USE_ESP8266 + AsyncWebServerResponse *response = request->beginResponse(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ)); +#else AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ)); +#endif response->addHeader("Content-Encoding", "gzip"); request->send(response); } #elif USE_WEBSERVER_VERSION >= 2 void WebServer::handle_index_request(AsyncWebServerRequest *request) { +#ifndef USE_ESP8266 + AsyncWebServerResponse *response = + request->beginResponse(200, "text/html", ESPHOME_WEBSERVER_INDEX_HTML, ESPHOME_WEBSERVER_INDEX_HTML_SIZE); +#else AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", ESPHOME_WEBSERVER_INDEX_HTML, ESPHOME_WEBSERVER_INDEX_HTML_SIZE); +#endif // No gzip header here because the HTML file is so small request->send(response); } @@ -317,8 +351,13 @@ void WebServer::handle_pna_cors_request(AsyncWebServerRequest *request) { #ifdef USE_WEBSERVER_CSS_INCLUDE void WebServer::handle_css_request(AsyncWebServerRequest *request) { +#ifndef USE_ESP8266 + AsyncWebServerResponse *response = + request->beginResponse(200, "text/css", ESPHOME_WEBSERVER_CSS_INCLUDE, ESPHOME_WEBSERVER_CSS_INCLUDE_SIZE); +#else AsyncWebServerResponse *response = request->beginResponse_P(200, "text/css", ESPHOME_WEBSERVER_CSS_INCLUDE, ESPHOME_WEBSERVER_CSS_INCLUDE_SIZE); +#endif response->addHeader("Content-Encoding", "gzip"); request->send(response); } @@ -326,8 +365,13 @@ void WebServer::handle_css_request(AsyncWebServerRequest *request) { #ifdef USE_WEBSERVER_JS_INCLUDE void WebServer::handle_js_request(AsyncWebServerRequest *request) { +#ifndef USE_ESP8266 + AsyncWebServerResponse *response = + request->beginResponse(200, "text/javascript", ESPHOME_WEBSERVER_JS_INCLUDE, ESPHOME_WEBSERVER_JS_INCLUDE_SIZE); +#else AsyncWebServerResponse *response = request->beginResponse_P(200, "text/javascript", ESPHOME_WEBSERVER_JS_INCLUDE, ESPHOME_WEBSERVER_JS_INCLUDE_SIZE); +#endif response->addHeader("Content-Encoding", "gzip"); request->send(response); } @@ -351,6 +395,12 @@ void WebServer::handle_js_request(AsyncWebServerRequest *request) { set_json_value(root, obj, sensor, value, start_config); \ (root)["state"] = state; +// Helper to get request detail parameter +static JsonDetail get_request_detail(AsyncWebServerRequest *request) { + auto *param = request->getParam("detail"); + return (param && param->value() == "all") ? DETAIL_ALL : DETAIL_STATE; +} + #ifdef USE_SENSOR void WebServer::on_sensor_update(sensor::Sensor *obj, float state) { if (this->events_.empty()) @@ -359,14 +409,10 @@ void WebServer::on_sensor_update(sensor::Sensor *obj, float state) { } void WebServer::handle_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (sensor::Sensor *obj : App.get_sensors()) { - if (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + if (request->method() == HTTP_GET && match.method_empty()) { + auto detail = get_request_detail(request); std::string data = this->sensor_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; @@ -392,12 +438,7 @@ std::string WebServer::sensor_json(sensor::Sensor *obj, float value, JsonDetail } set_json_icon_state_value(root, obj, "sensor-" + obj->get_object_id(), state, value, start_config); if (start_config == DETAIL_ALL) { - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); if (!obj->get_unit_of_measurement().empty()) root["uom"] = obj->get_unit_of_measurement(); } @@ -413,14 +454,10 @@ void WebServer::on_text_sensor_update(text_sensor::TextSensor *obj, const std::s } void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (text_sensor::TextSensor *obj : App.get_text_sensors()) { - if (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + if (request->method() == HTTP_GET && match.method_empty()) { + auto detail = get_request_detail(request); std::string data = this->text_sensor_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; @@ -441,12 +478,7 @@ std::string WebServer::text_sensor_json(text_sensor::TextSensor *obj, const std: return json::build_json([this, obj, value, start_config](JsonObject root) { set_json_icon_state_value(root, obj, "text_sensor-" + obj->get_object_id(), value, value, start_config); if (start_config == DETAIL_ALL) { - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -460,25 +492,21 @@ void WebServer::on_switch_update(switch_::Switch *obj, bool state) { } void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (switch_::Switch *obj : App.get_switches()) { - if (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + if (request->method() == HTTP_GET && match.method_empty()) { + auto detail = get_request_detail(request); std::string data = this->switch_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); - } else if (match.method == "toggle") { - this->schedule_([obj]() { obj->toggle(); }); + } else if (match.method_equals("toggle")) { + this->defer([obj]() { obj->toggle(); }); request->send(200); - } else if (match.method == "turn_on") { - this->schedule_([obj]() { obj->turn_on(); }); + } else if (match.method_equals("turn_on")) { + this->defer([obj]() { obj->turn_on(); }); request->send(200); - } else if (match.method == "turn_off") { - this->schedule_([obj]() { obj->turn_off(); }); + } else if (match.method_equals("turn_off")) { + this->defer([obj]() { obj->turn_off(); }); request->send(200); } else { request->send(404); @@ -498,12 +526,7 @@ std::string WebServer::switch_json(switch_::Switch *obj, bool value, JsonDetail set_json_icon_state_value(root, obj, "switch-" + obj->get_object_id(), value ? "ON" : "OFF", value, start_config); if (start_config == DETAIL_ALL) { root["assumed_state"] = obj->assumed_state(); - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -512,18 +535,14 @@ std::string WebServer::switch_json(switch_::Switch *obj, bool value, JsonDetail #ifdef USE_BUTTON void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (button::Button *obj : App.get_buttons()) { - if (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + if (request->method() == HTTP_GET && match.method_empty()) { + auto detail = get_request_detail(request); std::string data = this->button_json(obj, detail); request->send(200, "application/json", data.c_str()); - } else if (match.method == "press") { - this->schedule_([obj]() { obj->press(); }); + } else if (match.method_equals("press")) { + this->defer([obj]() { obj->press(); }); request->send(200); return; } else { @@ -543,33 +562,24 @@ std::string WebServer::button_json(button::Button *obj, JsonDetail start_config) return json::build_json([this, obj, start_config](JsonObject root) { set_json_id(root, obj, "button-" + obj->get_object_id(), start_config); if (start_config == DETAIL_ALL) { - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } #endif #ifdef USE_BINARY_SENSOR -void WebServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) { +void WebServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj) { if (this->events_.empty()) return; this->events_.deferrable_send_state(obj, "state", binary_sensor_state_json_generator); } void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (binary_sensor::BinarySensor *obj : App.get_binary_sensors()) { - if (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + if (request->method() == HTTP_GET && match.method_empty()) { + auto detail = get_request_detail(request); std::string data = this->binary_sensor_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; @@ -590,12 +600,7 @@ std::string WebServer::binary_sensor_json(binary_sensor::BinarySensor *obj, bool set_json_icon_state_value(root, obj, "binary_sensor-" + obj->get_object_id(), value ? "ON" : "OFF", value, start_config); if (start_config == DETAIL_ALL) { - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -609,22 +614,18 @@ void WebServer::on_fan_update(fan::Fan *obj) { } void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (fan::Fan *obj : App.get_fans()) { - if (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + if (request->method() == HTTP_GET && match.method_empty()) { + auto detail = get_request_detail(request); std::string data = this->fan_json(obj, detail); request->send(200, "application/json", data.c_str()); - } else if (match.method == "toggle") { - this->schedule_([obj]() { obj->toggle().perform(); }); + } else if (match.method_equals("toggle")) { + this->defer([obj]() { obj->toggle().perform(); }); request->send(200); - } else if (match.method == "turn_on" || match.method == "turn_off") { - auto call = match.method == "turn_on" ? obj->turn_on() : obj->turn_off(); + } else if (match.method_equals("turn_on") || match.method_equals("turn_off")) { + auto call = match.method_equals("turn_on") ? obj->turn_on() : obj->turn_off(); if (request->hasParam("speed_level")) { auto speed_level = request->getParam("speed_level")->value(); @@ -653,7 +654,7 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc return; } } - this->schedule_([call]() mutable { call.perform(); }); + this->defer([call]() mutable { call.perform(); }); request->send(200); } else { request->send(404); @@ -680,12 +681,7 @@ std::string WebServer::fan_json(fan::Fan *obj, JsonDetail start_config) { if (obj->get_traits().supports_oscillation()) root["oscillation"] = obj->oscillating; if (start_config == DETAIL_ALL) { - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -699,21 +695,17 @@ void WebServer::on_light_update(light::LightState *obj) { } void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (light::LightState *obj : App.get_lights()) { - if (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + if (request->method() == HTTP_GET && match.method_empty()) { + auto detail = get_request_detail(request); std::string data = this->light_json(obj, detail); request->send(200, "application/json", data.c_str()); - } else if (match.method == "toggle") { - this->schedule_([obj]() { obj->toggle().perform(); }); + } else if (match.method_equals("toggle")) { + this->defer([obj]() { obj->toggle().perform(); }); request->send(200); - } else if (match.method == "turn_on") { + } else if (match.method_equals("turn_on")) { auto call = obj->turn_on(); if (request->hasParam("brightness")) { auto brightness = parse_number(request->getParam("brightness")->value().c_str()); @@ -768,9 +760,9 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa call.set_effect(effect); } - this->schedule_([call]() mutable { call.perform(); }); + this->defer([call]() mutable { call.perform(); }); request->send(200); - } else if (match.method == "turn_off") { + } else if (match.method_equals("turn_off")) { auto call = obj->turn_off(); if (request->hasParam("transition")) { auto transition = parse_number(request->getParam("transition")->value().c_str()); @@ -778,7 +770,7 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa call.set_transition_length(*transition * 1000); } } - this->schedule_([call]() mutable { call.perform(); }); + this->defer([call]() mutable { call.perform(); }); request->send(200); } else { request->send(404); @@ -805,12 +797,7 @@ std::string WebServer::light_json(light::LightState *obj, JsonDetail start_confi for (auto const &option : obj->get_effects()) { opt.add(option->get_name()); } - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -824,30 +811,26 @@ void WebServer::on_cover_update(cover::Cover *obj) { } void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (cover::Cover *obj : App.get_covers()) { - if (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + if (request->method() == HTTP_GET && match.method_empty()) { + auto detail = get_request_detail(request); std::string data = this->cover_json(obj, detail); request->send(200, "application/json", data.c_str()); return; } auto call = obj->make_call(); - if (match.method == "open") { + if (match.method_equals("open")) { call.set_command_open(); - } else if (match.method == "close") { + } else if (match.method_equals("close")) { call.set_command_close(); - } else if (match.method == "stop") { + } else if (match.method_equals("stop")) { call.set_command_stop(); - } else if (match.method == "toggle") { + } else if (match.method_equals("toggle")) { call.set_command_toggle(); - } else if (match.method != "set") { + } else if (!match.method_equals("set")) { request->send(404); return; } @@ -872,7 +855,7 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa } } - this->schedule_([call]() mutable { call.perform(); }); + this->defer([call]() mutable { call.perform(); }); request->send(200); return; } @@ -895,12 +878,7 @@ std::string WebServer::cover_json(cover::Cover *obj, JsonDetail start_config) { if (obj->get_traits().get_supports_tilt()) root["tilt"] = obj->tilt; if (start_config == DETAIL_ALL) { - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -914,20 +892,16 @@ void WebServer::on_number_update(number::Number *obj, float state) { } void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_numbers()) { - if (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + if (request->method() == HTTP_GET && match.method_empty()) { + auto detail = get_request_detail(request); std::string data = this->number_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } - if (match.method != "set") { + if (!match.method_equals("set")) { request->send(404); return; } @@ -939,7 +913,7 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM call.set_value(*value); } - this->schedule_([call]() mutable { call.perform(); }); + this->defer([call]() mutable { call.perform(); }); request->send(200); return; } @@ -965,12 +939,7 @@ std::string WebServer::number_json(number::Number *obj, float value, JsonDetail root["mode"] = (int) obj->traits.get_mode(); if (!obj->traits.get_unit_of_measurement().empty()) root["uom"] = obj->traits.get_unit_of_measurement(); - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } if (std::isnan(value)) { root["value"] = "\"NaN\""; @@ -994,19 +963,15 @@ void WebServer::on_date_update(datetime::DateEntity *obj) { } void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_dates()) { - if (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + if (request->method() == HTTP_GET && match.method_empty()) { + auto detail = get_request_detail(request); std::string data = this->date_json(obj, detail); request->send(200, "application/json", data.c_str()); return; } - if (match.method != "set") { + if (!match.method_equals("set")) { request->send(404); return; } @@ -1023,7 +988,7 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat call.set_date(value); } - this->schedule_([call]() mutable { call.perform(); }); + this->defer([call]() mutable { call.perform(); }); request->send(200); return; } @@ -1043,12 +1008,7 @@ std::string WebServer::date_json(datetime::DateEntity *obj, JsonDetail start_con root["value"] = value; root["state"] = value; if (start_config == DETAIL_ALL) { - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -1062,19 +1022,15 @@ void WebServer::on_time_update(datetime::TimeEntity *obj) { } void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_times()) { - if (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + if (request->method() == HTTP_GET && match.method_empty()) { + auto detail = get_request_detail(request); std::string data = this->time_json(obj, detail); request->send(200, "application/json", data.c_str()); return; } - if (match.method != "set") { + if (!match.method_equals("set")) { request->send(404); return; } @@ -1091,7 +1047,7 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat call.set_time(value); } - this->schedule_([call]() mutable { call.perform(); }); + this->defer([call]() mutable { call.perform(); }); request->send(200); return; } @@ -1110,12 +1066,7 @@ std::string WebServer::time_json(datetime::TimeEntity *obj, JsonDetail start_con root["value"] = value; root["state"] = value; if (start_config == DETAIL_ALL) { - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -1129,19 +1080,15 @@ void WebServer::on_datetime_update(datetime::DateTimeEntity *obj) { } void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_datetimes()) { - if (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + if (request->method() == HTTP_GET && match.method_empty()) { + auto detail = get_request_detail(request); std::string data = this->datetime_json(obj, detail); request->send(200, "application/json", data.c_str()); return; } - if (match.method != "set") { + if (!match.method_equals("set")) { request->send(404); return; } @@ -1158,7 +1105,7 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur call.set_datetime(value); } - this->schedule_([call]() mutable { call.perform(); }); + this->defer([call]() mutable { call.perform(); }); request->send(200); return; } @@ -1178,12 +1125,7 @@ std::string WebServer::datetime_json(datetime::DateTimeEntity *obj, JsonDetail s root["value"] = value; root["state"] = value; if (start_config == DETAIL_ALL) { - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -1197,20 +1139,16 @@ void WebServer::on_text_update(text::Text *obj, const std::string &state) { } void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_texts()) { - if (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + if (request->method() == HTTP_GET && match.method_empty()) { + auto detail = get_request_detail(request); std::string data = this->text_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } - if (match.method != "set") { + if (!match.method_equals("set")) { request->send(404); return; } @@ -1248,12 +1186,7 @@ std::string WebServer::text_json(text::Text *obj, const std::string &value, Json root["value"] = value; if (start_config == DETAIL_ALL) { root["mode"] = (int) obj->traits.get_mode(); - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -1267,21 +1200,17 @@ void WebServer::on_select_update(select::Select *obj, const std::string &state, } void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_selects()) { - if (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + if (request->method() == HTTP_GET && match.method_empty()) { + auto detail = get_request_detail(request); std::string data = this->select_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } - if (match.method != "set") { + if (!match.method_equals("set")) { request->send(404); return; } @@ -1293,7 +1222,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM call.set_option(option.c_str()); // NOLINT } - this->schedule_([call]() mutable { call.perform(); }); + this->defer([call]() mutable { call.perform(); }); request->send(200); return; } @@ -1313,12 +1242,7 @@ std::string WebServer::select_json(select::Select *obj, const std::string &value for (auto &option : obj->traits.get_options()) { opt.add(option); } - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -1335,21 +1259,17 @@ void WebServer::on_climate_update(climate::Climate *obj) { } void WebServer::handle_climate_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_climates()) { - if (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + if (request->method() == HTTP_GET && match.method_empty()) { + auto detail = get_request_detail(request); std::string data = this->climate_json(obj, detail); request->send(200, "application/json", data.c_str()); return; } - if (match.method != "set") { + if (!match.method_equals("set")) { request->send(404); return; } @@ -1389,7 +1309,7 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url call.set_target_temperature(*target_temperature); } - this->schedule_([call]() mutable { call.perform(); }); + this->defer([call]() mutable { call.perform(); }); request->send(200); return; } @@ -1439,12 +1359,7 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf for (auto const &custom_preset : traits.get_supported_custom_presets()) opt.add(custom_preset); } - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } bool has_state = false; @@ -1503,25 +1418,21 @@ void WebServer::on_lock_update(lock::Lock *obj) { } void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (lock::Lock *obj : App.get_locks()) { - if (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + if (request->method() == HTTP_GET && match.method_empty()) { + auto detail = get_request_detail(request); std::string data = this->lock_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); - } else if (match.method == "lock") { - this->schedule_([obj]() { obj->lock(); }); + } else if (match.method_equals("lock")) { + this->defer([obj]() { obj->lock(); }); request->send(200); - } else if (match.method == "unlock") { - this->schedule_([obj]() { obj->unlock(); }); + } else if (match.method_equals("unlock")) { + this->defer([obj]() { obj->unlock(); }); request->send(200); - } else if (match.method == "open") { - this->schedule_([obj]() { obj->open(); }); + } else if (match.method_equals("open")) { + this->defer([obj]() { obj->open(); }); request->send(200); } else { request->send(404); @@ -1541,12 +1452,7 @@ std::string WebServer::lock_json(lock::Lock *obj, lock::LockState value, JsonDet set_json_icon_state_value(root, obj, "lock-" + obj->get_object_id(), lock::lock_state_to_string(value), value, start_config); if (start_config == DETAIL_ALL) { - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -1560,30 +1466,26 @@ void WebServer::on_valve_update(valve::Valve *obj) { } void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (valve::Valve *obj : App.get_valves()) { - if (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + if (request->method() == HTTP_GET && match.method_empty()) { + auto detail = get_request_detail(request); std::string data = this->valve_json(obj, detail); request->send(200, "application/json", data.c_str()); return; } auto call = obj->make_call(); - if (match.method == "open") { + if (match.method_equals("open")) { call.set_command_open(); - } else if (match.method == "close") { + } else if (match.method_equals("close")) { call.set_command_close(); - } else if (match.method == "stop") { + } else if (match.method_equals("stop")) { call.set_command_stop(); - } else if (match.method == "toggle") { + } else if (match.method_equals("toggle")) { call.set_command_toggle(); - } else if (match.method != "set") { + } else if (!match.method_equals("set")) { request->send(404); return; } @@ -1601,7 +1503,7 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa } } - this->schedule_([call]() mutable { call.perform(); }); + this->defer([call]() mutable { call.perform(); }); request->send(200); return; } @@ -1622,12 +1524,7 @@ std::string WebServer::valve_json(valve::Valve *obj, JsonDetail start_config) { if (obj->get_traits().get_supports_position()) root["position"] = obj->position; if (start_config == DETAIL_ALL) { - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -1641,15 +1538,11 @@ void WebServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlP } void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (alarm_control_panel::AlarmControlPanel *obj : App.get_alarm_control_panels()) { - if (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + if (request->method() == HTTP_GET && match.method_empty()) { + auto detail = get_request_detail(request); std::string data = this->alarm_control_panel_json(obj, obj->get_state(), detail); request->send(200, "application/json", data.c_str()); return; @@ -1660,22 +1553,22 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques call.set_code(request->getParam("code")->value().c_str()); // NOLINT } - if (match.method == "disarm") { + if (match.method_equals("disarm")) { call.disarm(); - } else if (match.method == "arm_away") { + } else if (match.method_equals("arm_away")) { call.arm_away(); - } else if (match.method == "arm_home") { + } else if (match.method_equals("arm_home")) { call.arm_home(); - } else if (match.method == "arm_night") { + } else if (match.method_equals("arm_night")) { call.arm_night(); - } else if (match.method == "arm_vacation") { + } else if (match.method_equals("arm_vacation")) { call.arm_vacation(); } else { request->send(404); return; } - this->schedule_([call]() mutable { call.perform(); }); + this->defer([call]() mutable { call.perform(); }); request->send(200); return; } @@ -1699,12 +1592,7 @@ std::string WebServer::alarm_control_panel_json(alarm_control_panel::AlarmContro set_json_icon_state_value(root, obj, "alarm-control-panel-" + obj->get_object_id(), PSTR_LOCAL(alarm_control_panel_state_to_string(value)), value, start_config); if (start_config == DETAIL_ALL) { - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -1717,15 +1605,11 @@ void WebServer::on_event(event::Event *obj, const std::string &event_type) { void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (event::Event *obj : App.get_events()) { - if (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + if (request->method() == HTTP_GET && match.method_empty()) { + auto detail = get_request_detail(request); std::string data = this->event_json(obj, "", detail); request->send(200, "application/json", data.c_str()); return; @@ -1756,12 +1640,7 @@ std::string WebServer::event_json(event::Event *obj, const std::string &event_ty event_types.add(event_type); } root["device_class"] = obj->get_device_class(); - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } @@ -1775,26 +1654,22 @@ void WebServer::on_update(update::UpdateEntity *obj) { } void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (update::UpdateEntity *obj : App.get_updates()) { - if (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { - auto detail = DETAIL_STATE; - auto *param = request->getParam("detail"); - if (param && param->value() == "all") { - detail = DETAIL_ALL; - } + if (request->method() == HTTP_GET && match.method_empty()) { + auto detail = get_request_detail(request); std::string data = this->update_json(obj, detail); request->send(200, "application/json", data.c_str()); return; } - if (match.method != "install") { + if (!match.method_equals("install")) { request->send(404); return; } - this->schedule_([obj]() mutable { obj->perform(); }); + this->defer([obj]() mutable { obj->perform(); }); request->send(200); return; } @@ -1829,18 +1704,13 @@ std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_c root["title"] = obj->update_info.title; root["summary"] = obj->update_info.summary; root["release_url"] = obj->update_info.release_url; - if (this->sorting_entitys_.find(obj) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[obj].weight; - if (this->sorting_groups_.find(this->sorting_entitys_[obj].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[obj].group_id].name; - } - } + this->add_sorting_info_(root, obj); } }); } #endif -bool WebServer::canHandle(AsyncWebServerRequest *request) { +bool WebServer::canHandle(AsyncWebServerRequest *request) const { if (request->url() == "/") return true; @@ -1862,116 +1732,114 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) { #ifdef USE_WEBSERVER_PRIVATE_NETWORK_ACCESS if (request->method() == HTTP_OPTIONS && request->hasHeader(HEADER_CORS_REQ_PNA)) { -#ifdef USE_ARDUINO - // Header needs to be added to interesting header list for it to not be - // nuked by the time we handle the request later. - // Only required in Arduino framework. - request->addInterestingHeader(HEADER_CORS_REQ_PNA); -#endif return true; } #endif - UrlMatch match = match_url(request->url().c_str(), true); // NOLINT + // Store the URL to prevent temporary string destruction + // request->url() returns a reference to a String (on Arduino) or std::string (on ESP-IDF) + // UrlMatch stores pointers to the string's data, so we must ensure the string outlives match_url() + const auto &url = request->url(); + UrlMatch match = match_url(url.c_str(), url.length(), true); if (!match.valid) return false; #ifdef USE_SENSOR - if (request->method() == HTTP_GET && match.domain == "sensor") + if (request->method() == HTTP_GET && match.domain_equals("sensor")) return true; #endif #ifdef USE_SWITCH - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "switch") + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("switch")) return true; #endif #ifdef USE_BUTTON - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "button") + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("button")) return true; #endif #ifdef USE_BINARY_SENSOR - if (request->method() == HTTP_GET && match.domain == "binary_sensor") + if (request->method() == HTTP_GET && match.domain_equals("binary_sensor")) return true; #endif #ifdef USE_FAN - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "fan") + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("fan")) return true; #endif #ifdef USE_LIGHT - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "light") + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("light")) return true; #endif #ifdef USE_TEXT_SENSOR - if (request->method() == HTTP_GET && match.domain == "text_sensor") + if (request->method() == HTTP_GET && match.domain_equals("text_sensor")) return true; #endif #ifdef USE_COVER - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "cover") + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("cover")) return true; #endif #ifdef USE_NUMBER - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "number") + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("number")) return true; #endif #ifdef USE_DATETIME_DATE - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "date") + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("date")) return true; #endif #ifdef USE_DATETIME_TIME - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "time") + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("time")) return true; #endif #ifdef USE_DATETIME_DATETIME - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "datetime") + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("datetime")) return true; #endif #ifdef USE_TEXT - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "text") + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("text")) return true; #endif #ifdef USE_SELECT - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "select") + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("select")) return true; #endif #ifdef USE_CLIMATE - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "climate") + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("climate")) return true; #endif #ifdef USE_LOCK - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "lock") + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("lock")) return true; #endif #ifdef USE_VALVE - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "valve") + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("valve")) return true; #endif #ifdef USE_ALARM_CONTROL_PANEL - if ((request->method() == HTTP_GET || request->method() == HTTP_POST) && match.domain == "alarm_control_panel") + if ((request->method() == HTTP_GET || request->method() == HTTP_POST) && match.domain_equals("alarm_control_panel")) return true; #endif #ifdef USE_EVENT - if (request->method() == HTTP_GET && match.domain == "event") + if (request->method() == HTTP_GET && match.domain_equals("event")) return true; #endif #ifdef USE_UPDATE - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "update") + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("update")) return true; #endif @@ -2011,114 +1879,117 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { } #endif - UrlMatch match = match_url(request->url().c_str()); // NOLINT + // See comment in canHandle() for why we store the URL reference + const auto &url = request->url(); + UrlMatch match = match_url(url.c_str(), url.length(), false); + #ifdef USE_SENSOR - if (match.domain == "sensor") { + if (match.domain_equals("sensor")) { this->handle_sensor_request(request, match); return; } #endif #ifdef USE_SWITCH - if (match.domain == "switch") { + if (match.domain_equals("switch")) { this->handle_switch_request(request, match); return; } #endif #ifdef USE_BUTTON - if (match.domain == "button") { + if (match.domain_equals("button")) { this->handle_button_request(request, match); return; } #endif #ifdef USE_BINARY_SENSOR - if (match.domain == "binary_sensor") { + if (match.domain_equals("binary_sensor")) { this->handle_binary_sensor_request(request, match); return; } #endif #ifdef USE_FAN - if (match.domain == "fan") { + if (match.domain_equals("fan")) { this->handle_fan_request(request, match); return; } #endif #ifdef USE_LIGHT - if (match.domain == "light") { + if (match.domain_equals("light")) { this->handle_light_request(request, match); return; } #endif #ifdef USE_TEXT_SENSOR - if (match.domain == "text_sensor") { + if (match.domain_equals("text_sensor")) { this->handle_text_sensor_request(request, match); return; } #endif #ifdef USE_COVER - if (match.domain == "cover") { + if (match.domain_equals("cover")) { this->handle_cover_request(request, match); return; } #endif #ifdef USE_NUMBER - if (match.domain == "number") { + if (match.domain_equals("number")) { this->handle_number_request(request, match); return; } #endif #ifdef USE_DATETIME_DATE - if (match.domain == "date") { + if (match.domain_equals("date")) { this->handle_date_request(request, match); return; } #endif #ifdef USE_DATETIME_TIME - if (match.domain == "time") { + if (match.domain_equals("time")) { this->handle_time_request(request, match); return; } #endif #ifdef USE_DATETIME_DATETIME - if (match.domain == "datetime") { + if (match.domain_equals("datetime")) { this->handle_datetime_request(request, match); return; } #endif #ifdef USE_TEXT - if (match.domain == "text") { + if (match.domain_equals("text")) { this->handle_text_request(request, match); return; } #endif #ifdef USE_SELECT - if (match.domain == "select") { + if (match.domain_equals("select")) { this->handle_select_request(request, match); return; } #endif #ifdef USE_CLIMATE - if (match.domain == "climate") { + if (match.domain_equals("climate")) { this->handle_climate_request(request, match); return; } #endif #ifdef USE_LOCK - if (match.domain == "lock") { + if (match.domain_equals("lock")) { this->handle_lock_request(request, match); return; @@ -2126,14 +1997,14 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { #endif #ifdef USE_VALVE - if (match.domain == "valve") { + if (match.domain_equals("valve")) { this->handle_valve_request(request, match); return; } #endif #ifdef USE_ALARM_CONTROL_PANEL - if (match.domain == "alarm_control_panel") { + if (match.domain_equals("alarm_control_panel")) { this->handle_alarm_control_panel_request(request, match); return; @@ -2141,15 +2012,31 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { #endif #ifdef USE_UPDATE - if (match.domain == "update") { + if (match.domain_equals("update")) { this->handle_update_request(request, match); return; } #endif + + // No matching handler found - send 404 + ESP_LOGV(TAG, "Request for unknown URL: %s", request->url().c_str()); + request->send(404, "text/plain", "Not Found"); } -bool WebServer::isRequestHandlerTrivial() { return false; } +bool WebServer::isRequestHandlerTrivial() const { return false; } +void WebServer::add_sorting_info_(JsonObject &root, EntityBase *entity) { +#ifdef USE_WEBSERVER_SORTING + if (this->sorting_entitys_.find(entity) != this->sorting_entitys_.end()) { + root["sorting_weight"] = this->sorting_entitys_[entity].weight; + if (this->sorting_groups_.find(this->sorting_entitys_[entity].group_id) != this->sorting_groups_.end()) { + root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[entity].group_id].name; + } + } +#endif +} + +#ifdef USE_WEBSERVER_SORTING void WebServer::add_entity_config(EntityBase *entity, float weight, uint64_t group) { this->sorting_entitys_[entity] = SortingComponents{weight, group}; } @@ -2157,16 +2044,7 @@ void WebServer::add_entity_config(EntityBase *entity, float weight, uint64_t gro void WebServer::add_sorting_group(uint64_t group_id, const std::string &group_name, float weight) { this->sorting_groups_[group_id] = SortingGroup{group_name, weight}; } - -void WebServer::schedule_(std::function &&f) { -#ifdef USE_ESP32 - xSemaphoreTake(this->to_schedule_lock_, portMAX_DELAY); - to_schedule_.push_back(std::move(f)); - xSemaphoreGive(this->to_schedule_lock_); -#else - this->defer(std::move(f)); #endif -} } // namespace web_server } // namespace esphome diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index e4f044c50b..0c15881d1e 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -14,11 +14,6 @@ #include #include #include -#ifdef USE_ESP32 -#include -#include -#include -#endif #if USE_WEBSERVER_VERSION >= 2 extern const uint8_t ESPHOME_WEBSERVER_INDEX_HTML[] PROGMEM; @@ -40,12 +35,31 @@ namespace web_server { /// Internal helper struct that is used to parse incoming URLs struct UrlMatch { - std::string domain; ///< The domain of the component, for example "sensor" - std::string id; ///< The id of the device that's being accessed, for example "living_room_fan" - std::string method; ///< The method that's being called, for example "turn_on" + const char *domain; ///< Pointer to domain within URL, for example "sensor" + const char *id; ///< Pointer to id within URL, for example "living_room_fan" + const char *method; ///< Pointer to method within URL, for example "turn_on" + uint8_t domain_len; ///< Length of domain string + uint8_t id_len; ///< Length of id string + uint8_t method_len; ///< Length of method string bool valid; ///< Whether this match is valid + + // Helper methods for string comparisons + bool domain_equals(const char *str) const { + return domain && domain_len == strlen(str) && memcmp(domain, str, domain_len) == 0; + } + + bool id_equals(const std::string &str) const { + return id && id_len == str.length() && memcmp(id, str.c_str(), id_len) == 0; + } + + bool method_equals(const char *str) const { + return method && method_len == strlen(str) && memcmp(method, str, method_len) == 0; + } + + bool method_empty() const { return method_len == 0; } }; +#ifdef USE_WEBSERVER_SORTING struct SortingComponents { float weight; uint64_t group_id; @@ -55,6 +69,7 @@ struct SortingGroup { std::string name; float weight; }; +#endif enum JsonDetail { DETAIL_ALL, DETAIL_STATE }; @@ -99,13 +114,15 @@ class DeferredUpdateEventSource : public AsyncEventSource { protected: // surface a couple methods from the base class using AsyncEventSource::handleRequest; - using AsyncEventSource::try_send; + using AsyncEventSource::send; ListEntitiesIterator entities_iterator_; // vector is used very specifically for its zero memory overhead even though items are popped from the front (memory // footprint is more important than speed here) std::vector deferred_queue_; WebServer *web_server_; + uint16_t consecutive_send_failures_{0}; + static constexpr uint16_t MAX_CONSECUTIVE_SEND_FAILURES = 2500; // ~20 seconds at 125Hz loop rate // helper for allowing only unique entries in the queue void deq_push_back_with_dedup_(void *source, message_generator_t *message_generator); @@ -192,11 +209,6 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { * @param include_internal Whether internal components should be displayed. */ void set_include_internal(bool include_internal) { include_internal_ = include_internal; } - /** Set whether or not the webserver should expose the OTA form and handler. - * - * @param allow_ota. - */ - void set_allow_ota(bool allow_ota) { this->allow_ota_ = allow_ota; } /** Set whether or not the webserver should expose the Log. * * @param expose_log. @@ -269,7 +281,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { #endif #ifdef USE_BINARY_SENSOR - void on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) override; + void on_binary_sensor_update(binary_sensor::BinarySensor *obj) override; /// Handle a binary sensor request under '/binary_sensor/'. void handle_binary_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -468,21 +480,24 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { #endif /// Override the web handler's canHandle method. - bool canHandle(AsyncWebServerRequest *request) override; + bool canHandle(AsyncWebServerRequest *request) const override; /// Override the web handler's handleRequest method. void handleRequest(AsyncWebServerRequest *request) override; /// This web handle is not trivial. - bool isRequestHandlerTrivial() override; // NOLINT(readability-identifier-naming) + bool isRequestHandlerTrivial() const override; // NOLINT(readability-identifier-naming) +#ifdef USE_WEBSERVER_SORTING void add_entity_config(EntityBase *entity, float weight, uint64_t group); void add_sorting_group(uint64_t group_id, const std::string &group_name, float weight); std::map sorting_entitys_; std::map sorting_groups_; +#endif + bool include_internal_{false}; protected: - void schedule_(std::function &&f); + void add_sorting_info_(JsonObject &root, EntityBase *entity); web_server_base::WebServerBase *base_; #ifdef USE_ARDUINO DeferredUpdateEventSourceList events_; @@ -501,12 +516,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { #ifdef USE_WEBSERVER_JS_INCLUDE const char *js_include_{nullptr}; #endif - bool allow_ota_{true}; bool expose_log_{true}; -#ifdef USE_ESP32 - std::deque> to_schedule_; - SemaphoreHandle_t to_schedule_lock_; -#endif }; } // namespace web_server diff --git a/esphome/components/web_server/web_server_v1.cpp b/esphome/components/web_server/web_server_v1.cpp index c9b38a2dc4..5db0f1cae9 100644 --- a/esphome/components/web_server/web_server_v1.cpp +++ b/esphome/components/web_server/web_server_v1.cpp @@ -192,11 +192,10 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) { stream->print(F("

See ESPHome Web API for " "REST API documentation.

")); - if (this->allow_ota_) { - stream->print( - F("

OTA Update

")); - } +#ifdef USE_WEBSERVER_OTA + stream->print(F("

OTA Update

")); +#endif stream->print(F("

Debug Log

"));
 #ifdef USE_WEBSERVER_JS_INCLUDE
   if (this->js_include_ != nullptr) {
diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py
index f50ee59b9c..754bf7d433 100644
--- a/esphome/components/web_server_base/__init__.py
+++ b/esphome/components/web_server_base/__init__.py
@@ -30,11 +30,14 @@ CONFIG_SCHEMA = cv.Schema(
 async def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
     await cg.register_component(var, config)
+    cg.add(cg.RawExpression(f"{web_server_base_ns}::global_web_server_base = {var}"))
 
     if CORE.using_arduino:
         if CORE.is_esp32:
             cg.add_library("WiFi", None)
             cg.add_library("FS", None)
             cg.add_library("Update", None)
-        # https://github.com/esphome/ESPAsyncWebServer/blob/master/library.json
-        cg.add_library("esphome/ESPAsyncWebServer-esphome", "3.3.0")
+        if CORE.is_esp8266:
+            cg.add_library("ESP8266WiFi", None)
+        # https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json
+        cg.add_library("ESP32Async/ESPAsyncWebServer", "3.7.8")
diff --git a/esphome/components/web_server_base/web_server_base.cpp b/esphome/components/web_server_base/web_server_base.cpp
index 2835585387..e1c2bc0b25 100644
--- a/esphome/components/web_server_base/web_server_base.cpp
+++ b/esphome/components/web_server_base/web_server_base.cpp
@@ -4,21 +4,13 @@
 #include "esphome/core/helpers.h"
 #include "esphome/core/log.h"
 
-#ifdef USE_ARDUINO
-#include 
-#if defined(USE_ESP32) || defined(USE_LIBRETINY)
-#include 
-#endif
-#ifdef USE_ESP8266
-#include 
-#endif
-#endif
-
 namespace esphome {
 namespace web_server_base {
 
 static const char *const TAG = "web_server_base";
 
+WebServerBase *global_web_server_base = nullptr;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
+
 void WebServerBase::add_handler(AsyncWebHandler *handler) {
   // remove all handlers
 
@@ -31,90 +23,6 @@ void WebServerBase::add_handler(AsyncWebHandler *handler) {
   }
 }
 
-void report_ota_error() {
-#ifdef USE_ARDUINO
-  StreamString ss;
-  Update.printError(ss);
-  ESP_LOGW(TAG, "OTA Update failed! Error: %s", ss.c_str());
-#endif
-}
-
-void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index,
-                                     uint8_t *data, size_t len, bool final) {
-#ifdef USE_ARDUINO
-  bool success;
-  if (index == 0) {
-    ESP_LOGI(TAG, "OTA Update Start: %s", filename.c_str());
-    this->ota_read_length_ = 0;
-#ifdef USE_ESP8266
-    Update.runAsync(true);
-    // NOLINTNEXTLINE(readability-static-accessed-through-instance)
-    success = Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000);
-#endif
-#if defined(USE_ESP32_FRAMEWORK_ARDUINO) || defined(USE_LIBRETINY)
-    if (Update.isRunning()) {
-      Update.abort();
-    }
-    success = Update.begin(UPDATE_SIZE_UNKNOWN, U_FLASH);
-#endif
-    if (!success) {
-      report_ota_error();
-      return;
-    }
-  } else if (Update.hasError()) {
-    // don't spam logs with errors if something failed at start
-    return;
-  }
-
-  success = Update.write(data, len) == len;
-  if (!success) {
-    report_ota_error();
-    return;
-  }
-  this->ota_read_length_ += len;
-
-  const uint32_t now = millis();
-  if (now - this->last_ota_progress_ > 1000) {
-    if (request->contentLength() != 0) {
-      float percentage = (this->ota_read_length_ * 100.0f) / request->contentLength();
-      ESP_LOGD(TAG, "OTA in progress: %0.1f%%", percentage);
-    } else {
-      ESP_LOGD(TAG, "OTA in progress: %u bytes read", this->ota_read_length_);
-    }
-    this->last_ota_progress_ = now;
-  }
-
-  if (final) {
-    if (Update.end(true)) {
-      ESP_LOGI(TAG, "OTA update successful!");
-      this->parent_->set_timeout(100, []() { App.safe_reboot(); });
-    } else {
-      report_ota_error();
-    }
-  }
-#endif
-}
-void OTARequestHandler::handleRequest(AsyncWebServerRequest *request) {
-#ifdef USE_ARDUINO
-  AsyncWebServerResponse *response;
-  if (!Update.hasError()) {
-    response = request->beginResponse(200, "text/plain", "Update Successful!");
-  } else {
-    StreamString ss;
-    ss.print("Update Failed: ");
-    Update.printError(ss);
-    response = request->beginResponse(200, "text/plain", ss);
-  }
-  response->addHeader("Connection", "close");
-  request->send(response);
-#endif
-}
-
-void WebServerBase::add_ota_handler() {
-#ifdef USE_ARDUINO
-  this->add_handler(new OTARequestHandler(this));  // NOLINT
-#endif
-}
 float WebServerBase::get_setup_priority() const {
   // Before WiFi (captive portal)
   return setup_priority::WIFI + 2.0f;
diff --git a/esphome/components/web_server_base/web_server_base.h b/esphome/components/web_server_base/web_server_base.h
index f876d163bc..a475238a37 100644
--- a/esphome/components/web_server_base/web_server_base.h
+++ b/esphome/components/web_server_base/web_server_base.h
@@ -17,13 +17,16 @@
 namespace esphome {
 namespace web_server_base {
 
+class WebServerBase;
+extern WebServerBase *global_web_server_base;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
+
 namespace internal {
 
 class MiddlewareHandler : public AsyncWebHandler {
  public:
   MiddlewareHandler(AsyncWebHandler *next) : next_(next) {}
 
-  bool canHandle(AsyncWebServerRequest *request) override { return next_->canHandle(request); }
+  bool canHandle(AsyncWebServerRequest *request) const override { return next_->canHandle(request); }
   void handleRequest(AsyncWebServerRequest *request) override { next_->handleRequest(request); }
   void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len,
                     bool final) override {
@@ -32,7 +35,7 @@ class MiddlewareHandler : public AsyncWebHandler {
   void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) override {
     next_->handleBody(request, data, len, index, total);
   }
-  bool isRequestHandlerTrivial() override { return next_->isRequestHandlerTrivial(); }
+  bool isRequestHandlerTrivial() const override { return next_->isRequestHandlerTrivial(); }
 
  protected:
   AsyncWebHandler *next_;
@@ -110,14 +113,10 @@ class WebServerBase : public Component {
 
   void add_handler(AsyncWebHandler *handler);
 
-  void add_ota_handler();
-
   void set_port(uint16_t port) { port_ = port; }
   uint16_t get_port() const { return port_; }
 
  protected:
-  friend class OTARequestHandler;
-
   int initialized_{0};
   uint16_t port_{80};
   std::shared_ptr server_{nullptr};
@@ -125,25 +124,6 @@ class WebServerBase : public Component {
   internal::Credentials credentials_;
 };
 
-class OTARequestHandler : public AsyncWebHandler {
- public:
-  OTARequestHandler(WebServerBase *parent) : parent_(parent) {}
-  void handleRequest(AsyncWebServerRequest *request) override;
-  void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len,
-                    bool final) override;
-  bool canHandle(AsyncWebServerRequest *request) override {
-    return request->url() == "/update" && request->method() == HTTP_POST;
-  }
-
-  // NOLINTNEXTLINE(readability-identifier-naming)
-  bool isRequestHandlerTrivial() override { return false; }
-
- protected:
-  uint32_t last_ota_progress_{0};
-  uint32_t ota_read_length_{0};
-  WebServerBase *parent_;
-};
-
 }  // namespace web_server_base
 }  // namespace esphome
 #endif
diff --git a/esphome/components/web_server_idf/multipart.cpp b/esphome/components/web_server_idf/multipart.cpp
new file mode 100644
index 0000000000..8655226ab9
--- /dev/null
+++ b/esphome/components/web_server_idf/multipart.cpp
@@ -0,0 +1,254 @@
+#include "esphome/core/defines.h"
+#if defined(USE_ESP_IDF) && defined(USE_WEBSERVER_OTA)
+#include "multipart.h"
+#include "utils.h"
+#include "esphome/core/log.h"
+#include 
+#include "multipart_parser.h"
+
+namespace esphome {
+namespace web_server_idf {
+
+static const char *const TAG = "multipart";
+
+// ========== MultipartReader Implementation ==========
+
+MultipartReader::MultipartReader(const std::string &boundary) {
+  // Initialize settings with callbacks
+  memset(&settings_, 0, sizeof(settings_));
+  settings_.on_header_field = on_header_field;
+  settings_.on_header_value = on_header_value;
+  settings_.on_part_data = on_part_data;
+  settings_.on_part_data_end = on_part_data_end;
+
+  ESP_LOGV(TAG, "Initializing multipart parser with boundary: '%s' (len: %zu)", boundary.c_str(), boundary.length());
+
+  // Create parser with boundary
+  parser_ = multipart_parser_init(boundary.c_str(), &settings_);
+  if (parser_) {
+    multipart_parser_set_data(parser_, this);
+  } else {
+    ESP_LOGE(TAG, "Failed to initialize multipart parser");
+  }
+}
+
+MultipartReader::~MultipartReader() {
+  if (parser_) {
+    multipart_parser_free(parser_);
+  }
+}
+
+size_t MultipartReader::parse(const char *data, size_t len) {
+  if (!parser_) {
+    ESP_LOGE(TAG, "Parser not initialized");
+    return 0;
+  }
+
+  size_t parsed = multipart_parser_execute(parser_, data, len);
+
+  if (parsed != len) {
+    ESP_LOGW(TAG, "Parser consumed %zu of %zu bytes - possible error", parsed, len);
+  }
+
+  return parsed;
+}
+
+void MultipartReader::process_header_(const char *value, size_t length) {
+  // Process the completed header (field + value pair)
+  std::string value_str(value, length);
+
+  if (str_startswith_case_insensitive(current_header_field_, "content-disposition")) {
+    // Parse name and filename from Content-Disposition
+    current_part_.name = extract_header_param(value_str, "name");
+    current_part_.filename = extract_header_param(value_str, "filename");
+  } else if (str_startswith_case_insensitive(current_header_field_, "content-type")) {
+    current_part_.content_type = str_trim(value_str);
+  }
+
+  // Clear field for next header
+  current_header_field_.clear();
+}
+
+int MultipartReader::on_header_field(multipart_parser *parser, const char *at, size_t length) {
+  MultipartReader *reader = static_cast(multipart_parser_get_data(parser));
+  reader->current_header_field_.assign(at, length);
+  return 0;
+}
+
+int MultipartReader::on_header_value(multipart_parser *parser, const char *at, size_t length) {
+  MultipartReader *reader = static_cast(multipart_parser_get_data(parser));
+  reader->process_header_(at, length);
+  return 0;
+}
+
+int MultipartReader::on_part_data(multipart_parser *parser, const char *at, size_t length) {
+  MultipartReader *reader = static_cast(multipart_parser_get_data(parser));
+  // Only process file uploads
+  if (reader->has_file() && reader->data_callback_) {
+    // IMPORTANT: The 'at' pointer points to data within the parser's input buffer.
+    // This data is only valid during this callback. The callback handler MUST
+    // process or copy the data immediately - it cannot store the pointer for
+    // later use as the buffer will be overwritten.
+    reader->data_callback_(reinterpret_cast(at), length);
+  }
+  return 0;
+}
+
+int MultipartReader::on_part_data_end(multipart_parser *parser) {
+  MultipartReader *reader = static_cast(multipart_parser_get_data(parser));
+  ESP_LOGV(TAG, "Part data end");
+  if (reader->part_complete_callback_) {
+    reader->part_complete_callback_();
+  }
+  // Clear part info for next part
+  reader->current_part_ = Part{};
+  return 0;
+}
+
+// ========== Utility Functions ==========
+
+// Case-insensitive string prefix check
+bool str_startswith_case_insensitive(const std::string &str, const std::string &prefix) {
+  if (str.length() < prefix.length()) {
+    return false;
+  }
+  return str_ncmp_ci(str.c_str(), prefix.c_str(), prefix.length());
+}
+
+// Extract a parameter value from a header line
+// Handles both quoted and unquoted values
+std::string extract_header_param(const std::string &header, const std::string ¶m) {
+  size_t search_pos = 0;
+
+  while (search_pos < header.length()) {
+    // Look for param name
+    const char *found = stristr(header.c_str() + search_pos, param.c_str());
+    if (!found) {
+      return "";
+    }
+    size_t pos = found - header.c_str();
+
+    // Check if this is a word boundary (not part of another parameter)
+    if (pos > 0 && header[pos - 1] != ' ' && header[pos - 1] != ';' && header[pos - 1] != '\t') {
+      search_pos = pos + 1;
+      continue;
+    }
+
+    // Move past param name
+    pos += param.length();
+
+    // Skip whitespace and find '='
+    while (pos < header.length() && (header[pos] == ' ' || header[pos] == '\t')) {
+      pos++;
+    }
+
+    if (pos >= header.length() || header[pos] != '=') {
+      search_pos = pos;
+      continue;
+    }
+
+    pos++;  // Skip '='
+
+    // Skip whitespace after '='
+    while (pos < header.length() && (header[pos] == ' ' || header[pos] == '\t')) {
+      pos++;
+    }
+
+    if (pos >= header.length()) {
+      return "";
+    }
+
+    // Check if value is quoted
+    if (header[pos] == '"') {
+      pos++;
+      size_t end = header.find('"', pos);
+      if (end != std::string::npos) {
+        return header.substr(pos, end - pos);
+      }
+      // Malformed - no closing quote
+      return "";
+    }
+
+    // Unquoted value - find the end (semicolon, comma, or end of string)
+    size_t end = pos;
+    while (end < header.length() && header[end] != ';' && header[end] != ',' && header[end] != ' ' &&
+           header[end] != '\t') {
+      end++;
+    }
+
+    return header.substr(pos, end - pos);
+  }
+
+  return "";
+}
+
+// Parse boundary from Content-Type header
+// Returns true if boundary found, false otherwise
+// boundary_start and boundary_len will point to the boundary value
+bool parse_multipart_boundary(const char *content_type, const char **boundary_start, size_t *boundary_len) {
+  if (!content_type) {
+    return false;
+  }
+
+  // Check for multipart/form-data (case-insensitive)
+  if (!stristr(content_type, "multipart/form-data")) {
+    return false;
+  }
+
+  // Look for boundary parameter
+  const char *b = stristr(content_type, "boundary=");
+  if (!b) {
+    return false;
+  }
+
+  const char *start = b + 9;  // Skip "boundary="
+
+  // Skip whitespace
+  while (*start == ' ' || *start == '\t') {
+    start++;
+  }
+
+  if (!*start) {
+    return false;
+  }
+
+  // Find end of boundary
+  const char *end = start;
+  if (*end == '"') {
+    // Quoted boundary
+    start++;
+    end++;
+    while (*end && *end != '"') {
+      end++;
+    }
+    *boundary_len = end - start;
+  } else {
+    // Unquoted boundary
+    while (*end && *end != ' ' && *end != ';' && *end != '\r' && *end != '\n' && *end != '\t') {
+      end++;
+    }
+    *boundary_len = end - start;
+  }
+
+  if (*boundary_len == 0) {
+    return false;
+  }
+
+  *boundary_start = start;
+
+  return true;
+}
+
+// Trim whitespace from both ends of a string
+std::string str_trim(const std::string &str) {
+  size_t start = str.find_first_not_of(" \t\r\n");
+  if (start == std::string::npos) {
+    return "";
+  }
+  size_t end = str.find_last_not_of(" \t\r\n");
+  return str.substr(start, end - start + 1);
+}
+
+}  // namespace web_server_idf
+}  // namespace esphome
+#endif  // defined(USE_ESP_IDF) && defined(USE_WEBSERVER_OTA)
diff --git a/esphome/components/web_server_idf/multipart.h b/esphome/components/web_server_idf/multipart.h
new file mode 100644
index 0000000000..967c72ffa5
--- /dev/null
+++ b/esphome/components/web_server_idf/multipart.h
@@ -0,0 +1,86 @@
+#pragma once
+#include "esphome/core/defines.h"
+#if defined(USE_ESP_IDF) && defined(USE_WEBSERVER_OTA)
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+namespace esphome {
+namespace web_server_idf {
+
+// Wrapper around zorxx/multipart-parser for ESP-IDF OTA uploads
+class MultipartReader {
+ public:
+  struct Part {
+    std::string name;
+    std::string filename;
+    std::string content_type;
+  };
+
+  // IMPORTANT: The data pointer in DataCallback is only valid during the callback!
+  // The multipart parser passes pointers to its internal buffer which will be
+  // overwritten after the callback returns. Callbacks MUST process or copy the
+  // data immediately - storing the pointer for deferred processing will result
+  // in use-after-free bugs.
+  using DataCallback = std::function;
+  using PartCompleteCallback = std::function;
+
+  explicit MultipartReader(const std::string &boundary);
+  ~MultipartReader();
+
+  // Set callbacks for handling data
+  void set_data_callback(DataCallback callback) { data_callback_ = std::move(callback); }
+  void set_part_complete_callback(PartCompleteCallback callback) { part_complete_callback_ = std::move(callback); }
+
+  // Parse incoming data
+  size_t parse(const char *data, size_t len);
+
+  // Get current part info
+  const Part &get_current_part() const { return current_part_; }
+
+  // Check if we found a file upload
+  bool has_file() const { return !current_part_.filename.empty(); }
+
+ private:
+  static int on_header_field(multipart_parser *parser, const char *at, size_t length);
+  static int on_header_value(multipart_parser *parser, const char *at, size_t length);
+  static int on_part_data(multipart_parser *parser, const char *at, size_t length);
+  static int on_part_data_end(multipart_parser *parser);
+
+  multipart_parser *parser_{nullptr};
+  multipart_parser_settings settings_{};
+
+  Part current_part_;
+  std::string current_header_field_;
+
+  DataCallback data_callback_;
+  PartCompleteCallback part_complete_callback_;
+
+  void process_header_(const char *value, size_t length);
+};
+
+// ========== Utility Functions ==========
+
+// Case-insensitive string prefix check
+bool str_startswith_case_insensitive(const std::string &str, const std::string &prefix);
+
+// Extract a parameter value from a header line
+// Handles both quoted and unquoted values
+std::string extract_header_param(const std::string &header, const std::string ¶m);
+
+// Parse boundary from Content-Type header
+// Returns true if boundary found, false otherwise
+// boundary_start and boundary_len will point to the boundary value
+bool parse_multipart_boundary(const char *content_type, const char **boundary_start, size_t *boundary_len);
+
+// Trim whitespace from both ends of a string
+std::string str_trim(const std::string &str);
+
+}  // namespace web_server_idf
+}  // namespace esphome
+#endif  // defined(USE_ESP_IDF) && defined(USE_WEBSERVER_OTA)
diff --git a/esphome/components/web_server_idf/utils.cpp b/esphome/components/web_server_idf/utils.cpp
index 349acce50d..ac5df90bb8 100644
--- a/esphome/components/web_server_idf/utils.cpp
+++ b/esphome/components/web_server_idf/utils.cpp
@@ -1,5 +1,7 @@
 #ifdef USE_ESP_IDF
 #include 
+#include 
+#include 
 #include "esphome/core/helpers.h"
 #include "esphome/core/log.h"
 #include "http_parser.h"
@@ -88,6 +90,36 @@ optional query_key_value(const std::string &query_url, const std::s
   return {val.get()};
 }
 
+// Helper function for case-insensitive string region comparison
+bool str_ncmp_ci(const char *s1, const char *s2, size_t n) {
+  for (size_t i = 0; i < n; i++) {
+    if (!char_equals_ci(s1[i], s2[i])) {
+      return false;
+    }
+  }
+  return true;
+}
+
+// Case-insensitive string search (like strstr but case-insensitive)
+const char *stristr(const char *haystack, const char *needle) {
+  if (!haystack) {
+    return nullptr;
+  }
+
+  size_t needle_len = strlen(needle);
+  if (needle_len == 0) {
+    return haystack;
+  }
+
+  for (const char *p = haystack; *p; p++) {
+    if (str_ncmp_ci(p, needle, needle_len)) {
+      return p;
+    }
+  }
+
+  return nullptr;
+}
+
 }  // namespace web_server_idf
 }  // namespace esphome
 #endif  // USE_ESP_IDF
diff --git a/esphome/components/web_server_idf/utils.h b/esphome/components/web_server_idf/utils.h
index 9ed17c1d50..988b962d72 100644
--- a/esphome/components/web_server_idf/utils.h
+++ b/esphome/components/web_server_idf/utils.h
@@ -2,6 +2,7 @@
 #ifdef USE_ESP_IDF
 
 #include 
+#include 
 #include "esphome/core/helpers.h"
 
 namespace esphome {
@@ -12,6 +13,15 @@ optional request_get_header(httpd_req_t *req, const char *name);
 optional request_get_url_query(httpd_req_t *req);
 optional query_key_value(const std::string &query_url, const std::string &key);
 
+// Helper function for case-insensitive character comparison
+inline bool char_equals_ci(char a, char b) { return ::tolower(a) == ::tolower(b); }
+
+// Helper function for case-insensitive string region comparison
+bool str_ncmp_ci(const char *s1, const char *s2, size_t n);
+
+// Case-insensitive string search (like strstr but case-insensitive)
+const char *stristr(const char *haystack, const char *needle);
+
 }  // namespace web_server_idf
 }  // namespace esphome
 #endif  // USE_ESP_IDF
diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp
index 90fdf720cd..d2447681f5 100644
--- a/esphome/components/web_server_idf/web_server_idf.cpp
+++ b/esphome/components/web_server_idf/web_server_idf.cpp
@@ -1,16 +1,25 @@
 #ifdef USE_ESP_IDF
 
 #include 
+#include 
+#include 
+#include 
 
 #include "esphome/core/helpers.h"
 #include "esphome/core/log.h"
 
 #include "esp_tls_crypto.h"
+#include 
+#include 
 
 #include "utils.h"
-
 #include "web_server_idf.h"
 
+#ifdef USE_WEBSERVER_OTA
+#include 
+#include "multipart.h"  // For parse_multipart_boundary and other utils
+#endif
+
 #ifdef USE_WEBSERVER
 #include "esphome/components/web_server/web_server.h"
 #include "esphome/components/web_server/list_entities.h"
@@ -28,6 +37,15 @@ namespace web_server_idf {
 
 static const char *const TAG = "web_server_idf";
 
+// Global instance to avoid guard variable (saves 8 bytes)
+// This is initialized at program startup before any threads
+namespace {
+// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
+DefaultHeaders default_headers_instance;
+}  // namespace
+
+DefaultHeaders &DefaultHeaders::Instance() { return default_headers_instance; }
+
 void AsyncWebServer::end() {
   if (this->server_) {
     httpd_stop(this->server_);
@@ -72,18 +90,32 @@ void AsyncWebServer::begin() {
 esp_err_t AsyncWebServer::request_post_handler(httpd_req_t *r) {
   ESP_LOGVV(TAG, "Enter AsyncWebServer::request_post_handler. uri=%s", r->uri);
   auto content_type = request_get_header(r, "Content-Type");
-  if (content_type.has_value() && *content_type != "application/x-www-form-urlencoded") {
-    ESP_LOGW(TAG, "Only application/x-www-form-urlencoded supported for POST request");
-    // fallback to get handler to support backward compatibility
-    return AsyncWebServer::request_handler(r);
-  }
 
   if (!request_has_header(r, "Content-Length")) {
-    ESP_LOGW(TAG, "Content length is requred for post: %s", r->uri);
+    ESP_LOGW(TAG, "Content length is required for post: %s", r->uri);
     httpd_resp_send_err(r, HTTPD_411_LENGTH_REQUIRED, nullptr);
     return ESP_OK;
   }
 
+  if (content_type.has_value()) {
+    const char *content_type_char = content_type.value().c_str();
+
+    // Check most common case first
+    if (stristr(content_type_char, "application/x-www-form-urlencoded") != nullptr) {
+      // Normal form data - proceed with regular handling
+#ifdef USE_WEBSERVER_OTA
+    } else if (stristr(content_type_char, "multipart/form-data") != nullptr) {
+      auto *server = static_cast(r->user_ctx);
+      return server->handle_multipart_upload_(r, content_type_char);
+#endif
+    } else {
+      ESP_LOGW(TAG, "Unsupported content type for POST: %s", content_type_char);
+      // fallback to get handler to support backward compatibility
+      return AsyncWebServer::request_handler(r);
+    }
+  }
+
+  // Handle regular form data
   if (r->content_len > HTTPD_MAX_REQ_HDR_LEN) {
     ESP_LOGW(TAG, "Request size is to big: %zu", r->content_len);
     httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr);
@@ -292,21 +324,38 @@ void AsyncEventSource::handleRequest(AsyncWebServerRequest *request) {
 }
 
 void AsyncEventSource::loop() {
-  for (auto *ses : this->sessions_) {
-    ses->loop();
+  // Clean up dead sessions safely
+  // This follows the ESP-IDF pattern where free_ctx marks resources as dead
+  // and the main loop handles the actual cleanup to avoid race conditions
+  auto it = this->sessions_.begin();
+  while (it != this->sessions_.end()) {
+    auto *ses = *it;
+    // If the session has a dead socket (marked by destroy callback)
+    if (ses->fd_.load() == 0) {
+      ESP_LOGD(TAG, "Removing dead event source session");
+      it = this->sessions_.erase(it);
+      delete ses;  // NOLINT(cppcoreguidelines-owning-memory)
+    } else {
+      ses->loop();
+      ++it;
+    }
   }
 }
 
 void AsyncEventSource::try_send_nodefer(const char *message, const char *event, uint32_t id, uint32_t reconnect) {
   for (auto *ses : this->sessions_) {
-    ses->try_send_nodefer(message, event, id, reconnect);
+    if (ses->fd_.load() != 0) {  // Skip dead sessions
+      ses->try_send_nodefer(message, event, id, reconnect);
+    }
   }
 }
 
 void AsyncEventSource::deferrable_send_state(void *source, const char *event_type,
                                              message_generator_t *message_generator) {
   for (auto *ses : this->sessions_) {
-    ses->deferrable_send_state(source, event_type, message_generator);
+    if (ses->fd_.load() != 0) {  // Skip dead sessions
+      ses->deferrable_send_state(source, event_type, message_generator);
+    }
   }
 }
 
@@ -331,13 +380,14 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest *
   req->free_ctx = AsyncEventSourceResponse::destroy;
 
   this->hd_ = req->handle;
-  this->fd_ = httpd_req_to_sockfd(req);
+  this->fd_.store(httpd_req_to_sockfd(req));
 
   // Configure reconnect timeout and send config
   // this should always go through since the tcp send buffer is empty on connect
   std::string message = ws->get_config_json();
   this->try_send_nodefer(message.c_str(), "ping", millis(), 30000);
 
+#ifdef USE_WEBSERVER_SORTING
   for (auto &group : ws->sorting_groups_) {
     message = json::build_json([group](JsonObject root) {
       root["name"] = group.second.name;
@@ -348,6 +398,7 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest *
     // since the only thing in the send buffer at this point is the initial ping/config
     this->try_send_nodefer(message.c_str(), "sorting_group");
   }
+#endif
 
   this->entities_iterator_->begin(ws->include_internal_);
 
@@ -360,8 +411,10 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest *
 
 void AsyncEventSourceResponse::destroy(void *ptr) {
   auto *rsp = static_cast(ptr);
-  rsp->server_->sessions_.erase(rsp);
-  delete rsp;  // NOLINT(cppcoreguidelines-owning-memory)
+  ESP_LOGD(TAG, "Event source connection closed (fd: %d)", rsp->fd_.load());
+  // Mark as dead by setting fd to 0 - will be cleaned up in the main loop
+  rsp->fd_.store(0);
+  // Note: We don't delete or remove from set here to avoid race conditions
 }
 
 // helper for allowing only unique entries in the queue
@@ -401,9 +454,11 @@ void AsyncEventSourceResponse::process_buffer_() {
     return;
   }
 
-  int bytes_sent = httpd_socket_send(this->hd_, this->fd_, event_buffer_.c_str() + event_bytes_sent_,
+  int bytes_sent = httpd_socket_send(this->hd_, this->fd_.load(), event_buffer_.c_str() + event_bytes_sent_,
                                      event_buffer_.size() - event_bytes_sent_, 0);
   if (bytes_sent == HTTPD_SOCK_ERR_TIMEOUT || bytes_sent == HTTPD_SOCK_ERR_FAIL) {
+    // Socket error - just return, the connection will be closed by httpd
+    // and our destroy callback will be called
     return;
   }
   event_bytes_sent_ += bytes_sent;
@@ -423,7 +478,7 @@ void AsyncEventSourceResponse::loop() {
 
 bool AsyncEventSourceResponse::try_send_nodefer(const char *message, const char *event, uint32_t id,
                                                 uint32_t reconnect) {
-  if (this->fd_ == 0) {
+  if (this->fd_.load() == 0) {
     return false;
   }
 
@@ -516,6 +571,97 @@ void AsyncEventSourceResponse::deferrable_send_state(void *source, const char *e
 }
 #endif
 
+#ifdef USE_WEBSERVER_OTA
+esp_err_t AsyncWebServer::handle_multipart_upload_(httpd_req_t *r, const char *content_type) {
+  static constexpr size_t MULTIPART_CHUNK_SIZE = 1460;       // Match Arduino AsyncWebServer buffer size
+  static constexpr size_t YIELD_INTERVAL_BYTES = 16 * 1024;  // Yield every 16KB to prevent watchdog
+
+  // Parse boundary and create reader
+  const char *boundary_start;
+  size_t boundary_len;
+  if (!parse_multipart_boundary(content_type, &boundary_start, &boundary_len)) {
+    ESP_LOGE(TAG, "Failed to parse multipart boundary");
+    httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr);
+    return ESP_FAIL;
+  }
+
+  AsyncWebServerRequest req(r);
+  AsyncWebHandler *handler = nullptr;
+  for (auto *h : this->handlers_) {
+    if (h->canHandle(&req)) {
+      handler = h;
+      break;
+    }
+  }
+
+  if (!handler) {
+    ESP_LOGW(TAG, "No handler found for OTA request");
+    httpd_resp_send_err(r, HTTPD_404_NOT_FOUND, nullptr);
+    return ESP_OK;
+  }
+
+  // Upload state
+  std::string filename;
+  size_t index = 0;
+  // Create reader on heap to reduce stack usage
+  auto reader = std::make_unique("--" + std::string(boundary_start, boundary_len));
+
+  // Configure callbacks
+  reader->set_data_callback([&](const uint8_t *data, size_t len) {
+    if (!reader->has_file() || !len)
+      return;
+
+    if (filename.empty()) {
+      filename = reader->get_current_part().filename;
+      ESP_LOGV(TAG, "Processing file: '%s'", filename.c_str());
+      handler->handleUpload(&req, filename, 0, nullptr, 0, false);  // Start
+    }
+
+    handler->handleUpload(&req, filename, index, const_cast(data), len, false);
+    index += len;
+  });
+
+  reader->set_part_complete_callback([&]() {
+    if (index > 0) {
+      handler->handleUpload(&req, filename, index, nullptr, 0, true);  // End
+      filename.clear();
+      index = 0;
+    }
+  });
+
+  // Process data
+  std::unique_ptr buffer(new char[MULTIPART_CHUNK_SIZE]);
+  size_t bytes_since_yield = 0;
+
+  for (size_t remaining = r->content_len; remaining > 0;) {
+    int recv_len = httpd_req_recv(r, buffer.get(), std::min(remaining, MULTIPART_CHUNK_SIZE));
+
+    if (recv_len <= 0) {
+      httpd_resp_send_err(r, recv_len == HTTPD_SOCK_ERR_TIMEOUT ? HTTPD_408_REQ_TIMEOUT : HTTPD_400_BAD_REQUEST,
+                          nullptr);
+      return recv_len == HTTPD_SOCK_ERR_TIMEOUT ? ESP_ERR_TIMEOUT : ESP_FAIL;
+    }
+
+    if (reader->parse(buffer.get(), recv_len) != static_cast(recv_len)) {
+      ESP_LOGW(TAG, "Multipart parser error");
+      httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr);
+      return ESP_FAIL;
+    }
+
+    remaining -= recv_len;
+    bytes_since_yield += recv_len;
+
+    if (bytes_since_yield > YIELD_INTERVAL_BYTES) {
+      vTaskDelay(1);
+      bytes_since_yield = 0;
+    }
+  }
+
+  handler->handleRequest(&req);
+  return ESP_OK;
+}
+#endif  // USE_WEBSERVER_OTA
+
 }  // namespace web_server_idf
 }  // namespace esphome
 
diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h
index d883c0ca9b..e8e40ef9b0 100644
--- a/esphome/components/web_server_idf/web_server_idf.h
+++ b/esphome/components/web_server_idf/web_server_idf.h
@@ -4,6 +4,7 @@
 #include "esphome/core/defines.h"
 #include 
 
+#include 
 #include 
 #include 
 #include 
@@ -135,8 +136,8 @@ class AsyncWebServerRequest {
     return res;
   }
   // NOLINTNEXTLINE(readability-identifier-naming)
-  AsyncWebServerResponse *beginResponse_P(int code, const char *content_type, const uint8_t *data,
-                                          const size_t data_size) {
+  AsyncWebServerResponse *beginResponse(int code, const char *content_type, const uint8_t *data,
+                                        const size_t data_size) {
     auto *res = new AsyncWebServerResponseProgmem(this, data, data_size);  // NOLINT(cppcoreguidelines-owning-memory)
     this->init_response_(res, code, content_type);
     return res;
@@ -203,6 +204,9 @@ class AsyncWebServer {
   static esp_err_t request_handler(httpd_req_t *r);
   static esp_err_t request_post_handler(httpd_req_t *r);
   esp_err_t request_handler_(AsyncWebServerRequest *request) const;
+#ifdef USE_WEBSERVER_OTA
+  esp_err_t handle_multipart_upload_(httpd_req_t *r, const char *content_type);
+#endif
   std::vector handlers_;
   std::function on_not_found_{};
 };
@@ -211,7 +215,7 @@ class AsyncWebHandler {
  public:
   virtual ~AsyncWebHandler() {}
   // NOLINTNEXTLINE(readability-identifier-naming)
-  virtual bool canHandle(AsyncWebServerRequest *request) { return false; }
+  virtual bool canHandle(AsyncWebServerRequest *request) const { return false; }
   // NOLINTNEXTLINE(readability-identifier-naming)
   virtual void handleRequest(AsyncWebServerRequest *request) {}
   // NOLINTNEXTLINE(readability-identifier-naming)
@@ -220,7 +224,7 @@ class AsyncWebHandler {
   // NOLINTNEXTLINE(readability-identifier-naming)
   virtual void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {}
   // NOLINTNEXTLINE(readability-identifier-naming)
-  virtual bool isRequestHandlerTrivial() { return true; }
+  virtual bool isRequestHandlerTrivial() const { return true; }
 };
 
 #ifdef USE_WEBSERVER
@@ -271,7 +275,7 @@ class AsyncEventSourceResponse {
   static void destroy(void *p);
   AsyncEventSource *server_;
   httpd_handle_t hd_{};
-  int fd_{};
+  std::atomic fd_{};
   std::vector deferred_queue_;
   esphome::web_server::WebServer *web_server_;
   std::unique_ptr entities_iterator_;
@@ -290,7 +294,7 @@ class AsyncEventSource : public AsyncWebHandler {
   ~AsyncEventSource() override;
 
   // NOLINTNEXTLINE(readability-identifier-naming)
-  bool canHandle(AsyncWebServerRequest *request) override {
+  bool canHandle(AsyncWebServerRequest *request) const override {
     return request->method() == HTTP_GET && request->url() == this->url_;
   }
   // NOLINTNEXTLINE(readability-identifier-naming)
@@ -324,10 +328,7 @@ class DefaultHeaders {
   void addHeader(const char *name, const char *value) { this->headers_.emplace_back(name, value); }
 
   // NOLINTNEXTLINE(readability-identifier-naming)
-  static DefaultHeaders &Instance() {
-    static DefaultHeaders instance;
-    return instance;
-  }
+  static DefaultHeaders &Instance();
 
  protected:
   std::vector> headers_;
diff --git a/esphome/components/wiegand/wiegand.cpp b/esphome/components/wiegand/wiegand.cpp
index 5a2bb8deee..dd1443d10c 100644
--- a/esphome/components/wiegand/wiegand.cpp
+++ b/esphome/components/wiegand/wiegand.cpp
@@ -11,7 +11,7 @@ static const char *const KEYS = "0123456789*#";
 void IRAM_ATTR HOT WiegandStore::d0_gpio_intr(WiegandStore *arg) {
   if (arg->d0.digital_read())
     return;
-  arg->count++;
+  arg->count++;  // NOLINT(clang-diagnostic-deprecated-volatile)
   arg->value <<= 1;
   arg->last_bit_time = millis();
   arg->done = false;
@@ -20,7 +20,7 @@ void IRAM_ATTR HOT WiegandStore::d0_gpio_intr(WiegandStore *arg) {
 void IRAM_ATTR HOT WiegandStore::d1_gpio_intr(WiegandStore *arg) {
   if (arg->d1.digital_read())
     return;
-  arg->count++;
+  arg->count++;  // NOLINT(clang-diagnostic-deprecated-volatile)
   arg->value = (arg->value << 1) | 1;
   arg->last_bit_time = millis();
   arg->done = false;
diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py
index 582b826de0..61f37556ba 100644
--- a/esphome/components/wifi/__init__.py
+++ b/esphome/components/wifi/__init__.py
@@ -3,6 +3,7 @@ from esphome.automation import Condition
 import esphome.codegen as cg
 from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant
 from esphome.components.network import IPAddress
+from esphome.config_helpers import filter_source_files_from_platform
 import esphome.config_validation as cv
 from esphome.const import (
     CONF_AP,
@@ -39,6 +40,7 @@ from esphome.const import (
     CONF_TTLS_PHASE_2,
     CONF_USE_ADDRESS,
     CONF_USERNAME,
+    PlatformFramework,
 )
 from esphome.core import CORE, HexInt, coroutine_with_priority
 import esphome.final_validate as fv
@@ -309,6 +311,7 @@ CONFIG_SCHEMA = cv.All(
                 rp2040="light",
                 bk72xx="none",
                 rtl87xx="none",
+                ln882x="light",
             ): cv.enum(WIFI_POWER_SAVE_MODES, upper=True),
             cv.Optional(CONF_FAST_CONNECT, default=False): cv.boolean,
             cv.Optional(CONF_USE_ADDRESS): cv.string_strict,
@@ -525,3 +528,18 @@ async def wifi_set_sta_to_code(config, action_id, template_arg, args):
         await automation.build_automation(var.get_error_trigger(), [], on_error_config)
     await cg.register_component(var, config)
     return var
+
+
+FILTER_SOURCE_FILES = filter_source_files_from_platform(
+    {
+        "wifi_component_esp32_arduino.cpp": {PlatformFramework.ESP32_ARDUINO},
+        "wifi_component_esp_idf.cpp": {PlatformFramework.ESP32_IDF},
+        "wifi_component_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
+        "wifi_component_libretiny.cpp": {
+            PlatformFramework.BK72XX_ARDUINO,
+            PlatformFramework.RTL87XX_ARDUINO,
+            PlatformFramework.LN882X_ARDUINO,
+        },
+        "wifi_component_pico_w.cpp": {PlatformFramework.RP2040_ARDUINO},
+    }
+)
diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp
index f2f9d712fc..d717b68340 100644
--- a/esphome/components/wifi/wifi_component.cpp
+++ b/esphome/components/wifi/wifi_component.cpp
@@ -73,7 +73,7 @@ void WiFiComponent::start() {
 
   SavedWifiSettings save{};
   if (this->pref_.load(&save)) {
-    ESP_LOGD(TAG, "Loaded saved settings: %s", save.ssid);
+    ESP_LOGD(TAG, "Loaded settings: %s", save.ssid);
 
     WiFiAP sta{};
     sta.set_ssid(save.ssid);
@@ -84,11 +84,11 @@ void WiFiComponent::start() {
   if (this->has_sta()) {
     this->wifi_sta_pre_setup_();
     if (this->output_power_.has_value() && !this->wifi_apply_output_power_(*this->output_power_)) {
-      ESP_LOGV(TAG, "Setting Output Power Option failed!");
+      ESP_LOGV(TAG, "Setting Output Power Option failed");
     }
 
     if (!this->wifi_apply_power_save_()) {
-      ESP_LOGV(TAG, "Setting Power Save Option failed!");
+      ESP_LOGV(TAG, "Setting Power Save Option failed");
     }
 
     if (this->fast_connect_) {
@@ -102,7 +102,7 @@ void WiFiComponent::start() {
   } else if (this->has_ap()) {
     this->setup_ap_config_();
     if (this->output_power_.has_value() && !this->wifi_apply_output_power_(*this->output_power_)) {
-      ESP_LOGV(TAG, "Setting Output Power Option failed!");
+      ESP_LOGV(TAG, "Setting Output Power Option failed");
     }
 #ifdef USE_CAPTIVE_PORTAL
     if (captive_portal::global_captive_portal != nullptr) {
@@ -181,7 +181,7 @@ void WiFiComponent::loop() {
 #ifdef USE_WIFI_AP
     if (this->has_ap() && !this->ap_setup_) {
       if (this->ap_timeout_ != 0 && (now - this->last_connected_ > this->ap_timeout_)) {
-        ESP_LOGI(TAG, "Starting fallback AP!");
+        ESP_LOGI(TAG, "Starting fallback AP");
         this->setup_ap_config_();
 #ifdef USE_CAPTIVE_PORTAL
         if (captive_portal::global_captive_portal != nullptr)
@@ -359,7 +359,7 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) {
   if (ap.get_channel().has_value()) {
     ESP_LOGV(TAG, "  Channel: %u", *ap.get_channel());
   } else {
-    ESP_LOGV(TAG, "  Channel: Not Set");
+    ESP_LOGV(TAG, "  Channel not set");
   }
   if (ap.get_manual_ip().has_value()) {
     ManualIP m = *ap.get_manual_ip();
@@ -372,7 +372,7 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) {
 #endif
 
   if (!this->wifi_sta_connect_(ap)) {
-    ESP_LOGE(TAG, "wifi_sta_connect_ failed!");
+    ESP_LOGE(TAG, "wifi_sta_connect_ failed");
     this->retry_connect();
     return;
   }
@@ -500,20 +500,20 @@ void WiFiComponent::start_scanning() {
 void WiFiComponent::check_scanning_finished() {
   if (!this->scan_done_) {
     if (millis() - this->action_started_ > 30000) {
-      ESP_LOGE(TAG, "Scan timeout!");
+      ESP_LOGE(TAG, "Scan timeout");
       this->retry_connect();
     }
     return;
   }
   this->scan_done_ = false;
 
-  ESP_LOGD(TAG, "Found networks:");
   if (this->scan_result_.empty()) {
-    ESP_LOGD(TAG, "  No network found!");
+    ESP_LOGW(TAG, "No networks found");
     this->retry_connect();
     return;
   }
 
+  ESP_LOGD(TAG, "Found networks:");
   for (auto &res : this->scan_result_) {
     for (auto &ap : this->sta_) {
       if (res.matches(ap)) {
@@ -561,7 +561,7 @@ void WiFiComponent::check_scanning_finished() {
   }
 
   if (!this->scan_result_[0].get_matches()) {
-    ESP_LOGW(TAG, "No matching network found!");
+    ESP_LOGW(TAG, "No matching network found");
     this->retry_connect();
     return;
   }
@@ -619,7 +619,7 @@ void WiFiComponent::check_connecting_finished() {
 
   if (status == WiFiSTAConnectStatus::CONNECTED) {
     if (wifi_ssid().empty()) {
-      ESP_LOGW(TAG, "Incomplete connection.");
+      ESP_LOGW(TAG, "Connection incomplete");
       this->retry_connect();
       return;
     }
@@ -663,7 +663,7 @@ void WiFiComponent::check_connecting_finished() {
   }
 
   if (this->error_from_callback_) {
-    ESP_LOGW(TAG, "Error while connecting to network.");
+    ESP_LOGW(TAG, "Connecting to network failed");
     this->retry_connect();
     return;
   }
@@ -679,7 +679,7 @@ void WiFiComponent::check_connecting_finished() {
   }
 
   if (status == WiFiSTAConnectStatus::ERROR_CONNECT_FAILED) {
-    ESP_LOGW(TAG, "Connection failed. Check credentials");
+    ESP_LOGW(TAG, "Connecting to network failed");
     this->retry_connect();
     return;
   }
@@ -700,7 +700,7 @@ void WiFiComponent::retry_connect() {
       (this->num_retried_ > 3 || this->error_from_callback_)) {
     if (this->num_retried_ > 5) {
       // If retry failed for more than 5 times, let's restart STA
-      ESP_LOGW(TAG, "Restarting WiFi adapter");
+      ESP_LOGW(TAG, "Restarting adapter");
       this->wifi_mode_(false, {});
       delay(100);  // NOLINT
       this->num_retried_ = 0;
@@ -741,11 +741,6 @@ void WiFiComponent::set_power_save_mode(WiFiPowerSaveMode power_save) { this->po
 
 void WiFiComponent::set_passive_scan(bool passive) { this->passive_scan_ = passive; }
 
-std::string WiFiComponent::format_mac_addr(const uint8_t *mac) {
-  char buf[20];
-  sprintf(buf, "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
-  return buf;
-}
 bool WiFiComponent::is_captive_portal_active_() {
 #ifdef USE_CAPTIVE_PORTAL
   return captive_portal::global_captive_portal != nullptr && captive_portal::global_captive_portal->is_active();
@@ -770,7 +765,7 @@ void WiFiComponent::load_fast_connect_settings_() {
     this->selected_ap_.set_bssid(bssid);
     this->selected_ap_.set_channel(fast_connect_save.channel);
 
-    ESP_LOGD(TAG, "Loaded saved fast_connect wifi settings");
+    ESP_LOGD(TAG, "Loaded fast_connect settings");
   }
 }
 
@@ -786,7 +781,7 @@ void WiFiComponent::save_fast_connect_settings_() {
 
     this->fast_connect_pref_.save(&fast_connect_save);
 
-    ESP_LOGD(TAG, "Saved fast_connect wifi settings");
+    ESP_LOGD(TAG, "Saved fast_connect settings");
   }
 }
 
diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h
index 982007e47f..64797a5801 100644
--- a/esphome/components/wifi/wifi_component.h
+++ b/esphome/components/wifi/wifi_component.h
@@ -62,7 +62,7 @@ struct SavedWifiFastConnectSettings {
   uint8_t channel;
 } PACKED;  // NOLINT
 
-enum WiFiComponentState {
+enum WiFiComponentState : uint8_t {
   /** Nothing has been initialized yet. Internal AP, if configured, is disabled at this point. */
   WIFI_COMPONENT_STATE_OFF = 0,
   /** WiFi is disabled. */
@@ -146,14 +146,14 @@ class WiFiAP {
 
  protected:
   std::string ssid_;
-  optional bssid_;
   std::string password_;
+  optional bssid_;
 #ifdef USE_WIFI_WPA2_EAP
   optional eap_;
 #endif  // USE_WIFI_WPA2_EAP
-  optional channel_;
-  float priority_{0};
   optional manual_ip_;
+  float priority_{0};
+  optional channel_;
   bool hidden_{false};
 };
 
@@ -177,14 +177,14 @@ class WiFiScanResult {
   bool operator==(const WiFiScanResult &rhs) const;
 
  protected:
-  bool matches_{false};
   bssid_t bssid_;
   std::string ssid_;
+  float priority_{0.0f};
   uint8_t channel_;
   int8_t rssi_;
+  bool matches_{false};
   bool with_auth_;
   bool is_hidden_;
-  float priority_{0.0f};
 };
 
 struct WiFiSTAPriority {
@@ -192,7 +192,7 @@ struct WiFiSTAPriority {
   float priority;
 };
 
-enum WiFiPowerSaveMode {
+enum WiFiPowerSaveMode : uint8_t {
   WIFI_POWER_SAVE_NONE = 0,
   WIFI_POWER_SAVE_LIGHT,
   WIFI_POWER_SAVE_HIGH,
@@ -321,8 +321,6 @@ class WiFiComponent : public Component {
   int32_t get_wifi_channel();
 
  protected:
-  static std::string format_mac_addr(const uint8_t mac[6]);
-
 #ifdef USE_WIFI_AP
   void setup_ap_config_();
 #endif  // USE_WIFI_AP
@@ -385,28 +383,36 @@ class WiFiComponent : public Component {
   std::string use_address_;
   std::vector sta_;
   std::vector sta_priorities_;
+  std::vector scan_result_;
   WiFiAP selected_ap_;
-  bool fast_connect_{false};
-  bool retry_hidden_{false};
-
-  bool has_ap_{false};
   WiFiAP ap_;
-  WiFiComponentState state_{WIFI_COMPONENT_STATE_OFF};
-  bool handled_connected_state_{false};
+  optional output_power_;
+  ESPPreferenceObject pref_;
+  ESPPreferenceObject fast_connect_pref_;
+
+  // Group all 32-bit integers together
   uint32_t action_started_;
-  uint8_t num_retried_{0};
   uint32_t last_connected_{0};
   uint32_t reboot_timeout_{};
   uint32_t ap_timeout_{};
+
+  // Group all 8-bit values together
+  WiFiComponentState state_{WIFI_COMPONENT_STATE_OFF};
   WiFiPowerSaveMode power_save_{WIFI_POWER_SAVE_NONE};
+  uint8_t num_retried_{0};
+#if USE_NETWORK_IPV6
+  uint8_t num_ipv6_addresses_{0};
+#endif /* USE_NETWORK_IPV6 */
+
+  // Group all boolean values together
+  bool fast_connect_{false};
+  bool retry_hidden_{false};
+  bool has_ap_{false};
+  bool handled_connected_state_{false};
   bool error_from_callback_{false};
-  std::vector scan_result_;
   bool scan_done_{false};
   bool ap_setup_{false};
-  optional output_power_;
   bool passive_scan_{false};
-  ESPPreferenceObject pref_;
-  ESPPreferenceObject fast_connect_pref_;
   bool has_saved_wifi_settings_{false};
 #ifdef USE_WIFI_11KV_SUPPORT
   bool btm_{false};
@@ -414,10 +420,8 @@ class WiFiComponent : public Component {
 #endif
   bool enable_on_boot_;
   bool got_ipv4_address_{false};
-#if USE_NETWORK_IPV6
-  uint8_t num_ipv6_addresses_{0};
-#endif /* USE_NETWORK_IPV6 */
 
+  // Pointers at the end (naturally aligned)
   Trigger<> *connect_trigger_{new Trigger<>()};
   Trigger<> *disconnect_trigger_{new Trigger<>()};
 };
diff --git a/esphome/components/wifi/wifi_component_esp32_arduino.cpp b/esphome/components/wifi/wifi_component_esp32_arduino.cpp
index 2dc3acda77..a7877eb90b 100644
--- a/esphome/components/wifi/wifi_component_esp32_arduino.cpp
+++ b/esphome/components/wifi/wifi_component_esp32_arduino.cpp
@@ -9,7 +9,7 @@
 #include 
 #include 
 #ifdef USE_WIFI_WPA2_EAP
-#include 
+#include 
 #endif
 
 #ifdef USE_WIFI_AP
@@ -78,14 +78,14 @@ bool WiFiComponent::wifi_mode_(optional sta, optional ap) {
     return true;
 
   if (set_sta && !current_sta) {
-    ESP_LOGV(TAG, "Enabling STA.");
+    ESP_LOGV(TAG, "Enabling STA");
   } else if (!set_sta && current_sta) {
-    ESP_LOGV(TAG, "Disabling STA.");
+    ESP_LOGV(TAG, "Disabling STA");
   }
   if (set_ap && !current_ap) {
-    ESP_LOGV(TAG, "Enabling AP.");
+    ESP_LOGV(TAG, "Enabling AP");
   } else if (!set_ap && current_ap) {
-    ESP_LOGV(TAG, "Disabling AP.");
+    ESP_LOGV(TAG, "Disabling AP");
   }
 
   bool ret = WiFiClass::mode(set_mode);
@@ -147,11 +147,11 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
   wifi_config_t conf;
   memset(&conf, 0, sizeof(conf));
   if (ap.get_ssid().size() > sizeof(conf.sta.ssid)) {
-    ESP_LOGE(TAG, "SSID is too long");
+    ESP_LOGE(TAG, "SSID too long");
     return false;
   }
   if (ap.get_password().size() > sizeof(conf.sta.password)) {
-    ESP_LOGE(TAG, "password is too long");
+    ESP_LOGE(TAG, "Password too long");
     return false;
   }
   memcpy(reinterpret_cast(conf.sta.ssid), ap.get_ssid().c_str(), ap.get_ssid().size());
@@ -228,43 +228,43 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
   if (ap.get_eap().has_value()) {
     // note: all certificates and keys have to be null terminated. Lengths are appended by +1 to include \0.
     EAPAuth eap = ap.get_eap().value();
-    err = esp_wifi_sta_wpa2_ent_set_identity((uint8_t *) eap.identity.c_str(), eap.identity.length());
+    err = esp_eap_client_set_identity((uint8_t *) eap.identity.c_str(), eap.identity.length());
     if (err != ESP_OK) {
-      ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_identity failed! %d", err);
+      ESP_LOGV(TAG, "esp_eap_client_set_identity failed: %d", err);
     }
     int ca_cert_len = strlen(eap.ca_cert);
     int client_cert_len = strlen(eap.client_cert);
     int client_key_len = strlen(eap.client_key);
     if (ca_cert_len) {
-      err = esp_wifi_sta_wpa2_ent_set_ca_cert((uint8_t *) eap.ca_cert, ca_cert_len + 1);
+      err = esp_eap_client_set_ca_cert((uint8_t *) eap.ca_cert, ca_cert_len + 1);
       if (err != ESP_OK) {
-        ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_ca_cert failed! %d", err);
+        ESP_LOGV(TAG, "esp_eap_client_set_ca_cert failed: %d", err);
       }
     }
     // workout what type of EAP this is
     // validation is not required as the config tool has already validated it
     if (client_cert_len && client_key_len) {
       // if we have certs, this must be EAP-TLS
-      err = esp_wifi_sta_wpa2_ent_set_cert_key((uint8_t *) eap.client_cert, client_cert_len + 1,
-                                               (uint8_t *) eap.client_key, client_key_len + 1,
-                                               (uint8_t *) eap.password.c_str(), strlen(eap.password.c_str()));
+      err = esp_eap_client_set_certificate_and_key((uint8_t *) eap.client_cert, client_cert_len + 1,
+                                                   (uint8_t *) eap.client_key, client_key_len + 1,
+                                                   (uint8_t *) eap.password.c_str(), strlen(eap.password.c_str()));
       if (err != ESP_OK) {
-        ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_cert_key failed! %d", err);
+        ESP_LOGV(TAG, "esp_eap_client_set_certificate_and_key failed: %d", err);
       }
     } else {
       // in the absence of certs, assume this is username/password based
-      err = esp_wifi_sta_wpa2_ent_set_username((uint8_t *) eap.username.c_str(), eap.username.length());
+      err = esp_eap_client_set_username((uint8_t *) eap.username.c_str(), eap.username.length());
       if (err != ESP_OK) {
-        ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_username failed! %d", err);
+        ESP_LOGV(TAG, "esp_eap_client_set_username failed: %d", err);
       }
-      err = esp_wifi_sta_wpa2_ent_set_password((uint8_t *) eap.password.c_str(), eap.password.length());
+      err = esp_eap_client_set_password((uint8_t *) eap.password.c_str(), eap.password.length());
       if (err != ESP_OK) {
-        ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_password failed! %d", err);
+        ESP_LOGV(TAG, "esp_eap_client_set_password failed: %d", err);
       }
     }
-    err = esp_wifi_sta_wpa2_ent_enable();
+    err = esp_wifi_sta_enterprise_enable();
     if (err != ESP_OK) {
-      ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_enable failed! %d", err);
+      ESP_LOGV(TAG, "esp_wifi_sta_enterprise_enable failed: %d", err);
     }
   }
 #endif  // USE_WIFI_WPA2_EAP
@@ -319,7 +319,7 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) {
     if (dhcp_status != ESP_NETIF_DHCP_STARTED) {
       err = esp_netif_dhcpc_start(s_sta_netif);
       if (err != ESP_OK) {
-        ESP_LOGV(TAG, "Starting DHCP client failed! %d", err);
+        ESP_LOGV(TAG, "Starting DHCP client failed: %d", err);
       }
       return err == ESP_OK;
     }
@@ -332,12 +332,12 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) {
   info.netmask = manual_ip->subnet;
   err = esp_netif_dhcpc_stop(s_sta_netif);
   if (err != ESP_OK && err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STOPPED) {
-    ESP_LOGV(TAG, "Stopping DHCP client failed! %s", esp_err_to_name(err));
+    ESP_LOGV(TAG, "Stopping DHCP client failed: %s", esp_err_to_name(err));
   }
 
   err = esp_netif_set_ip_info(s_sta_netif, &info);
   if (err != ESP_OK) {
-    ESP_LOGV(TAG, "Setting manual IP info failed! %s", esp_err_to_name(err));
+    ESP_LOGV(TAG, "Setting manual IP info failed: %s", esp_err_to_name(err));
   }
 
   esp_netif_dns_info_t dns;
@@ -520,18 +520,18 @@ using esphome_wifi_event_info_t = arduino_event_info_t;
 void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_wifi_event_info_t info) {
   switch (event) {
     case ESPHOME_EVENT_ID_WIFI_READY: {
-      ESP_LOGV(TAG, "Event: WiFi ready");
+      ESP_LOGV(TAG, "Ready");
       break;
     }
     case ESPHOME_EVENT_ID_WIFI_SCAN_DONE: {
       auto it = info.wifi_scan_done;
-      ESP_LOGV(TAG, "Event: WiFi Scan Done status=%u number=%u scan_id=%u", it.status, it.number, it.scan_id);
+      ESP_LOGV(TAG, "Scan done: status=%u number=%u scan_id=%u", it.status, it.number, it.scan_id);
 
       this->wifi_scan_done_callback_();
       break;
     }
     case ESPHOME_EVENT_ID_WIFI_STA_START: {
-      ESP_LOGV(TAG, "Event: WiFi STA start");
+      ESP_LOGV(TAG, "STA start");
       // apply hostname
       s_sta_netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
       esp_err_t err = esp_netif_set_hostname(s_sta_netif, App.get_name().c_str());
@@ -541,7 +541,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_
       break;
     }
     case ESPHOME_EVENT_ID_WIFI_STA_STOP: {
-      ESP_LOGV(TAG, "Event: WiFi STA stop");
+      ESP_LOGV(TAG, "STA stop");
       break;
     }
     case ESPHOME_EVENT_ID_WIFI_STA_CONNECTED: {
@@ -549,10 +549,10 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_
       char buf[33];
       memcpy(buf, it.ssid, it.ssid_len);
       buf[it.ssid_len] = '\0';
-      ESP_LOGV(TAG, "Event: Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf,
-               format_mac_addr(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode));
+      ESP_LOGV(TAG, "Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf,
+               format_mac_address_pretty(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode));
 #if USE_NETWORK_IPV6
-      this->set_timeout(100, [] { WiFi.enableIpV6(); });
+      this->set_timeout(100, [] { WiFi.enableIPv6(); });
 #endif /* USE_NETWORK_IPV6 */
 
       break;
@@ -563,10 +563,10 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_
       memcpy(buf, it.ssid, it.ssid_len);
       buf[it.ssid_len] = '\0';
       if (it.reason == WIFI_REASON_NO_AP_FOUND) {
-        ESP_LOGW(TAG, "Event: Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf);
+        ESP_LOGW(TAG, "Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf);
       } else {
-        ESP_LOGW(TAG, "Event: Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf,
-                 format_mac_addr(it.bssid).c_str(), get_disconnect_reason_str(it.reason));
+        ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf,
+                 format_mac_address_pretty(it.bssid).c_str(), get_disconnect_reason_str(it.reason));
       }
 
       uint8_t reason = it.reason;
@@ -585,8 +585,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_
     }
     case ESPHOME_EVENT_ID_WIFI_STA_AUTHMODE_CHANGE: {
       auto it = info.wifi_sta_authmode_change;
-      ESP_LOGV(TAG, "Event: Authmode Change old=%s new=%s", get_auth_mode_str(it.old_mode),
-               get_auth_mode_str(it.new_mode));
+      ESP_LOGV(TAG, "Authmode Change old=%s new=%s", get_auth_mode_str(it.old_mode), get_auth_mode_str(it.new_mode));
       // Mitigate CVE-2020-12638
       // https://lbsfilm.at/blog/wpa2-authenticationmode-downgrade-in-espressif-microprocessors
       if (it.old_mode != WIFI_AUTH_OPEN && it.new_mode == WIFI_AUTH_OPEN) {
@@ -603,8 +602,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_
     }
     case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP: {
       auto it = info.got_ip.ip_info;
-      ESP_LOGV(TAG, "Event: Got IP static_ip=%s gateway=%s", format_ip4_addr(it.ip).c_str(),
-               format_ip4_addr(it.gw).c_str());
+      ESP_LOGV(TAG, "static_ip=%s gateway=%s", format_ip4_addr(it.ip).c_str(), format_ip4_addr(it.gw).c_str());
       this->got_ipv4_address_ = true;
 #if USE_NETWORK_IPV6
       s_sta_connecting = this->num_ipv6_addresses_ < USE_NETWORK_MIN_IPV6_ADDR_COUNT;
@@ -616,44 +614,44 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_
 #if USE_NETWORK_IPV6
     case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP6: {
       auto it = info.got_ip6.ip6_info;
-      ESP_LOGV(TAG, "Got IPv6 address=" IPV6STR, IPV62STR(it.ip));
+      ESP_LOGV(TAG, "IPv6 address=" IPV6STR, IPV62STR(it.ip));
       this->num_ipv6_addresses_++;
       s_sta_connecting = !(this->got_ipv4_address_ & (this->num_ipv6_addresses_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT));
       break;
     }
 #endif /* USE_NETWORK_IPV6 */
     case ESPHOME_EVENT_ID_WIFI_STA_LOST_IP: {
-      ESP_LOGV(TAG, "Event: Lost IP");
+      ESP_LOGV(TAG, "Lost IP");
       this->got_ipv4_address_ = false;
       break;
     }
     case ESPHOME_EVENT_ID_WIFI_AP_START: {
-      ESP_LOGV(TAG, "Event: WiFi AP start");
+      ESP_LOGV(TAG, "AP start");
       break;
     }
     case ESPHOME_EVENT_ID_WIFI_AP_STOP: {
-      ESP_LOGV(TAG, "Event: WiFi AP stop");
+      ESP_LOGV(TAG, "AP stop");
       break;
     }
     case ESPHOME_EVENT_ID_WIFI_AP_STACONNECTED: {
       auto it = info.wifi_sta_connected;
       auto &mac = it.bssid;
-      ESP_LOGV(TAG, "Event: AP client connected MAC=%s", format_mac_addr(mac).c_str());
+      ESP_LOGV(TAG, "AP client connected MAC=%s", format_mac_address_pretty(mac).c_str());
       break;
     }
     case ESPHOME_EVENT_ID_WIFI_AP_STADISCONNECTED: {
       auto it = info.wifi_sta_disconnected;
       auto &mac = it.bssid;
-      ESP_LOGV(TAG, "Event: AP client disconnected MAC=%s", format_mac_addr(mac).c_str());
+      ESP_LOGV(TAG, "AP client disconnected MAC=%s", format_mac_address_pretty(mac).c_str());
       break;
     }
     case ESPHOME_EVENT_ID_WIFI_AP_STAIPASSIGNED: {
-      ESP_LOGV(TAG, "Event: AP client assigned IP");
+      ESP_LOGV(TAG, "AP client assigned IP");
       break;
     }
     case ESPHOME_EVENT_ID_WIFI_AP_PROBEREQRECVED: {
       auto it = info.wifi_ap_probereqrecved;
-      ESP_LOGVV(TAG, "Event: AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi);
+      ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", format_mac_address_pretty(it.mac).c_str(), it.rssi);
       break;
     }
     default:
@@ -662,12 +660,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_
 }
 
 WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() {
-#if USE_ARDUINO_VERSION_CODE < VERSION_CODE(3, 1, 0)
-  const auto status = WiFiClass::status();
-#else
   const auto status = WiFi.status();
-#endif
-
   if (status == WL_CONNECT_FAILED || status == WL_CONNECTION_LOST) {
     return WiFiSTAConnectStatus::ERROR_CONNECT_FAILED;
   }
@@ -690,7 +683,7 @@ bool WiFiComponent::wifi_scan_start_(bool passive) {
   // need to use WiFi because of WiFiScanClass allocations :(
   int16_t err = WiFi.scanNetworks(true, true, passive, 200);
   if (err != WIFI_SCAN_RUNNING) {
-    ESP_LOGV(TAG, "WiFi.scanNetworks failed! %d", err);
+    ESP_LOGV(TAG, "WiFi.scanNetworks failed: %d", err);
     return false;
   }
 
@@ -746,7 +739,7 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) {
 
   err = esp_netif_set_ip_info(s_ap_netif, &info);
   if (err != ESP_OK) {
-    ESP_LOGE(TAG, "esp_netif_set_ip_info failed! %d", err);
+    ESP_LOGE(TAG, "esp_netif_set_ip_info failed: %d", err);
     return false;
   }
 
@@ -762,14 +755,14 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) {
   err = esp_netif_dhcps_option(s_ap_netif, ESP_NETIF_OP_SET, ESP_NETIF_REQUESTED_IP_ADDRESS, &lease, sizeof(lease));
 
   if (err != ESP_OK) {
-    ESP_LOGE(TAG, "esp_netif_dhcps_option failed! %d", err);
+    ESP_LOGE(TAG, "esp_netif_dhcps_option failed: %d", err);
     return false;
   }
 
   err = esp_netif_dhcps_start(s_ap_netif);
 
   if (err != ESP_OK) {
-    ESP_LOGE(TAG, "esp_netif_dhcps_start failed! %d", err);
+    ESP_LOGE(TAG, "esp_netif_dhcps_start failed: %d", err);
     return false;
   }
 
@@ -784,7 +777,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
   wifi_config_t conf;
   memset(&conf, 0, sizeof(conf));
   if (ap.get_ssid().size() > sizeof(conf.ap.ssid)) {
-    ESP_LOGE(TAG, "AP SSID is too long");
+    ESP_LOGE(TAG, "AP SSID too long");
     return false;
   }
   memcpy(reinterpret_cast(conf.ap.ssid), ap.get_ssid().c_str(), ap.get_ssid().size());
@@ -799,7 +792,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
   } else {
     conf.ap.authmode = WIFI_AUTH_WPA2_PSK;
     if (ap.get_password().size() > sizeof(conf.ap.password)) {
-      ESP_LOGE(TAG, "AP password is too long");
+      ESP_LOGE(TAG, "AP password too long");
       return false;
     }
     memcpy(reinterpret_cast(conf.ap.password), ap.get_password().c_str(), ap.get_password().size());
@@ -810,14 +803,14 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
 
   esp_err_t err = esp_wifi_set_config(WIFI_IF_AP, &conf);
   if (err != ESP_OK) {
-    ESP_LOGV(TAG, "esp_wifi_set_config failed! %d", err);
+    ESP_LOGV(TAG, "esp_wifi_set_config failed: %d", err);
     return false;
   }
 
   yield();
 
   if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) {
-    ESP_LOGV(TAG, "wifi_ap_ip_config_ failed!");
+    ESP_LOGV(TAG, "wifi_ap_ip_config_ failed");
     return false;
   }
 
diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp
index 3e121098e7..ae1daed8b5 100644
--- a/esphome/components/wifi/wifi_component_esp8266.cpp
+++ b/esphome/components/wifi/wifi_component_esp8266.cpp
@@ -59,17 +59,17 @@ bool WiFiComponent::wifi_mode_(optional sta, optional ap) {
     return true;
 
   if (target_sta && !current_sta) {
-    ESP_LOGV(TAG, "Enabling STA.");
+    ESP_LOGV(TAG, "Enabling STA");
   } else if (!target_sta && current_sta) {
-    ESP_LOGV(TAG, "Disabling STA.");
+    ESP_LOGV(TAG, "Disabling STA");
     // Stop DHCP client when disabling STA
     // See https://github.com/esp8266/Arduino/pull/5703
     wifi_station_dhcpc_stop();
   }
   if (target_ap && !current_ap) {
-    ESP_LOGV(TAG, "Enabling AP.");
+    ESP_LOGV(TAG, "Enabling AP");
   } else if (!target_ap && current_ap) {
-    ESP_LOGV(TAG, "Disabling AP.");
+    ESP_LOGV(TAG, "Disabling AP");
   }
 
   ETS_UART_INTR_DISABLE();
@@ -82,7 +82,7 @@ bool WiFiComponent::wifi_mode_(optional sta, optional ap) {
   ETS_UART_INTR_ENABLE();
 
   if (!ret) {
-    ESP_LOGW(TAG, "Setting WiFi mode failed!");
+    ESP_LOGW(TAG, "Set mode failed");
   }
 
   return ret;
@@ -133,7 +133,7 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) {
     if (dhcp_status != DHCP_STARTED) {
       bool ret = wifi_station_dhcpc_start();
       if (!ret) {
-        ESP_LOGV(TAG, "Starting DHCP client failed!");
+        ESP_LOGV(TAG, "Starting DHCP client failed");
       }
       return ret;
     }
@@ -157,13 +157,13 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) {
   if (dhcp_status == DHCP_STARTED) {
     bool dhcp_stop_ret = wifi_station_dhcpc_stop();
     if (!dhcp_stop_ret) {
-      ESP_LOGV(TAG, "Stopping DHCP client failed!");
+      ESP_LOGV(TAG, "Stopping DHCP client failed");
       ret = false;
     }
   }
   bool wifi_set_info_ret = wifi_set_ip_info(STATION_IF, &info);
   if (!wifi_set_info_ret) {
-    ESP_LOGV(TAG, "Setting manual IP info failed!");
+    ESP_LOGV(TAG, "Set manual IP info failed");
     ret = false;
   }
 
@@ -202,7 +202,7 @@ bool WiFiComponent::wifi_apply_hostname_() {
   const std::string &hostname = App.get_name();
   bool ret = wifi_station_set_hostname(const_cast(hostname.c_str()));
   if (!ret) {
-    ESP_LOGV(TAG, "Setting WiFi Hostname failed!");
+    ESP_LOGV(TAG, "Set hostname failed");
   }
 
   // inform dhcp server of hostname change using dhcp_renew()
@@ -237,11 +237,11 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
   struct station_config conf {};
   memset(&conf, 0, sizeof(conf));
   if (ap.get_ssid().size() > sizeof(conf.ssid)) {
-    ESP_LOGE(TAG, "SSID is too long");
+    ESP_LOGE(TAG, "SSID too long");
     return false;
   }
   if (ap.get_password().size() > sizeof(conf.password)) {
-    ESP_LOGE(TAG, "password is too long");
+    ESP_LOGE(TAG, "Password too long");
     return false;
   }
   memcpy(reinterpret_cast(conf.ssid), ap.get_ssid().c_str(), ap.get_ssid().size());
@@ -269,7 +269,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
   ETS_UART_INTR_ENABLE();
 
   if (!ret) {
-    ESP_LOGV(TAG, "Setting WiFi Station config failed!");
+    ESP_LOGV(TAG, "Set Station config failed");
     return false;
   }
 
@@ -284,7 +284,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
     EAPAuth eap = ap.get_eap().value();
     ret = wifi_station_set_enterprise_identity((uint8_t *) eap.identity.c_str(), eap.identity.length());
     if (ret) {
-      ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_identity failed! %d", ret);
+      ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_identity failed: %d", ret);
     }
     int ca_cert_len = strlen(eap.ca_cert);
     int client_cert_len = strlen(eap.client_cert);
@@ -292,7 +292,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
     if (ca_cert_len) {
       ret = wifi_station_set_enterprise_ca_cert((uint8_t *) eap.ca_cert, ca_cert_len + 1);
       if (ret) {
-        ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_ca_cert failed! %d", ret);
+        ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_ca_cert failed: %d", ret);
       }
     }
     // workout what type of EAP this is
@@ -303,22 +303,22 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
                                                  (uint8_t *) eap.client_key, client_key_len + 1,
                                                  (uint8_t *) eap.password.c_str(), strlen(eap.password.c_str()));
       if (ret) {
-        ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_cert_key failed! %d", ret);
+        ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_cert_key failed: %d", ret);
       }
     } else {
       // in the absence of certs, assume this is username/password based
       ret = wifi_station_set_enterprise_username((uint8_t *) eap.username.c_str(), eap.username.length());
       if (ret) {
-        ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_username failed! %d", ret);
+        ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_username failed: %d", ret);
       }
       ret = wifi_station_set_enterprise_password((uint8_t *) eap.password.c_str(), eap.password.length());
       if (ret) {
-        ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_password failed! %d", ret);
+        ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_password failed: %d", ret);
       }
     }
     ret = wifi_station_set_wpa2_enterprise_auth(true);
     if (ret) {
-      ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_enable failed! %d", ret);
+      ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_enable failed: %d", ret);
     }
   }
 #endif  // USE_WIFI_WPA2_EAP
@@ -337,7 +337,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
   ret = wifi_station_connect();
   ETS_UART_INTR_ENABLE();
   if (!ret) {
-    ESP_LOGV(TAG, "wifi_station_connect failed!");
+    ESP_LOGV(TAG, "wifi_station_connect failed");
     return false;
   }
 
@@ -359,7 +359,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
   if (ap.get_channel().has_value()) {
     ret = wifi_set_channel(*ap.get_channel());
     if (!ret) {
-      ESP_LOGV(TAG, "wifi_set_channel failed!");
+      ESP_LOGV(TAG, "wifi_set_channel failed");
       return false;
     }
   }
@@ -496,7 +496,7 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
       char buf[33];
       memcpy(buf, it.ssid, it.ssid_len);
       buf[it.ssid_len] = '\0';
-      ESP_LOGV(TAG, "Event: Connected ssid='%s' bssid=%s channel=%u", buf, format_mac_addr(it.bssid).c_str(),
+      ESP_LOGV(TAG, "Connected ssid='%s' bssid=%s channel=%u", buf, format_mac_address_pretty(it.bssid).c_str(),
                it.channel);
       s_sta_connected = true;
       break;
@@ -507,11 +507,11 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
       memcpy(buf, it.ssid, it.ssid_len);
       buf[it.ssid_len] = '\0';
       if (it.reason == REASON_NO_AP_FOUND) {
-        ESP_LOGW(TAG, "Event: Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf);
+        ESP_LOGW(TAG, "Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf);
         s_sta_connect_not_found = true;
       } else {
-        ESP_LOGW(TAG, "Event: Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf,
-                 format_mac_addr(it.bssid).c_str(), LOG_STR_ARG(get_disconnect_reason_str(it.reason)));
+        ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf,
+                 format_mac_address_pretty(it.bssid).c_str(), LOG_STR_ARG(get_disconnect_reason_str(it.reason)));
         s_sta_connect_error = true;
       }
       s_sta_connected = false;
@@ -520,7 +520,7 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
     }
     case EVENT_STAMODE_AUTHMODE_CHANGE: {
       auto it = event->event_info.auth_change;
-      ESP_LOGV(TAG, "Event: Changed AuthMode old=%s new=%s", LOG_STR_ARG(get_auth_mode_str(it.old_mode)),
+      ESP_LOGV(TAG, "Changed Authmode old=%s new=%s", LOG_STR_ARG(get_auth_mode_str(it.old_mode)),
                LOG_STR_ARG(get_auth_mode_str(it.new_mode)));
       // Mitigate CVE-2020-12638
       // https://lbsfilm.at/blog/wpa2-authenticationmode-downgrade-in-espressif-microprocessors
@@ -535,40 +535,40 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
     }
     case EVENT_STAMODE_GOT_IP: {
       auto it = event->event_info.got_ip;
-      ESP_LOGV(TAG, "Event: Got IP static_ip=%s gateway=%s netmask=%s", format_ip_addr(it.ip).c_str(),
-               format_ip_addr(it.gw).c_str(), format_ip_addr(it.mask).c_str());
+      ESP_LOGV(TAG, "static_ip=%s gateway=%s netmask=%s", format_ip_addr(it.ip).c_str(), format_ip_addr(it.gw).c_str(),
+               format_ip_addr(it.mask).c_str());
       s_sta_got_ip = true;
       break;
     }
     case EVENT_STAMODE_DHCP_TIMEOUT: {
-      ESP_LOGW(TAG, "Event: Getting IP address timeout");
+      ESP_LOGW(TAG, "DHCP request timeout");
       break;
     }
     case EVENT_SOFTAPMODE_STACONNECTED: {
       auto it = event->event_info.sta_connected;
-      ESP_LOGV(TAG, "Event: AP client connected MAC=%s aid=%u", format_mac_addr(it.mac).c_str(), it.aid);
+      ESP_LOGV(TAG, "AP client connected MAC=%s aid=%u", format_mac_address_pretty(it.mac).c_str(), it.aid);
       break;
     }
     case EVENT_SOFTAPMODE_STADISCONNECTED: {
       auto it = event->event_info.sta_disconnected;
-      ESP_LOGV(TAG, "Event: AP client disconnected MAC=%s aid=%u", format_mac_addr(it.mac).c_str(), it.aid);
+      ESP_LOGV(TAG, "AP client disconnected MAC=%s aid=%u", format_mac_address_pretty(it.mac).c_str(), it.aid);
       break;
     }
     case EVENT_SOFTAPMODE_PROBEREQRECVED: {
       auto it = event->event_info.ap_probereqrecved;
-      ESP_LOGVV(TAG, "Event: AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi);
+      ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", format_mac_address_pretty(it.mac).c_str(), it.rssi);
       break;
     }
 #if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0)
     case EVENT_OPMODE_CHANGED: {
       auto it = event->event_info.opmode_changed;
-      ESP_LOGV(TAG, "Event: Changed Mode old=%s new=%s", LOG_STR_ARG(get_op_mode_str(it.old_opmode)),
+      ESP_LOGV(TAG, "Changed Mode old=%s new=%s", LOG_STR_ARG(get_op_mode_str(it.old_opmode)),
                LOG_STR_ARG(get_op_mode_str(it.new_opmode)));
       break;
     }
     case EVENT_SOFTAPMODE_DISTRIBUTE_STA_IP: {
       auto it = event->event_info.distribute_sta_ip;
-      ESP_LOGV(TAG, "Event: AP Distribute Station IP MAC=%s IP=%s aid=%u", format_mac_addr(it.mac).c_str(),
+      ESP_LOGV(TAG, "AP Distribute Station IP MAC=%s IP=%s aid=%u", format_mac_address_pretty(it.mac).c_str(),
                format_ip_addr(it.ip).c_str(), it.aid);
       break;
     }
@@ -600,7 +600,7 @@ bool WiFiComponent::wifi_sta_pre_setup_() {
   ETS_UART_INTR_ENABLE();
 
   if (!ret1 || !ret2) {
-    ESP_LOGV(TAG, "Disabling Auto-Connect failed!");
+    ESP_LOGV(TAG, "Disabling Auto-Connect failed");
   }
 
   delay(10);
@@ -666,7 +666,7 @@ bool WiFiComponent::wifi_scan_start_(bool passive) {
   first_scan = false;
   bool ret = wifi_station_scan(&config, &WiFiComponent::s_wifi_scan_done_callback);
   if (!ret) {
-    ESP_LOGV(TAG, "wifi_station_scan failed!");
+    ESP_LOGV(TAG, "wifi_station_scan failed");
     return false;
   }
 
@@ -692,7 +692,7 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) {
   this->scan_result_.clear();
 
   if (status != OK) {
-    ESP_LOGV(TAG, "Scan failed! %d", status);
+    ESP_LOGV(TAG, "Scan failed: %d", status);
     this->retry_connect();
     return;
   }
@@ -725,12 +725,12 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) {
 
   if (wifi_softap_dhcps_status() == DHCP_STARTED) {
     if (!wifi_softap_dhcps_stop()) {
-      ESP_LOGW(TAG, "Stopping DHCP server failed!");
+      ESP_LOGW(TAG, "Stopping DHCP server failed");
     }
   }
 
   if (!wifi_set_ip_info(SOFTAP_IF, &info)) {
-    ESP_LOGE(TAG, "Setting SoftAP info failed!");
+    ESP_LOGE(TAG, "Set SoftAP info failed");
     return false;
   }
 
@@ -748,13 +748,13 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) {
   lease.end_ip = start_address;
   ESP_LOGV(TAG, "DHCP server IP lease end: %s", start_address.str().c_str());
   if (!wifi_softap_set_dhcps_lease(&lease)) {
-    ESP_LOGE(TAG, "Setting SoftAP DHCP lease failed!");
+    ESP_LOGE(TAG, "Set SoftAP DHCP lease failed");
     return false;
   }
 
   // lease time 1440 minutes (=24 hours)
   if (!wifi_softap_set_dhcps_lease_time(1440)) {
-    ESP_LOGE(TAG, "Setting SoftAP DHCP lease time failed!");
+    ESP_LOGE(TAG, "Set SoftAP DHCP lease time failed");
     return false;
   }
 
@@ -764,13 +764,13 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) {
   uint8_t mode = 1;
   // bit0, 1 enables router information from ESP8266 SoftAP DHCP server.
   if (!wifi_softap_set_dhcps_offer_option(OFFER_ROUTER, &mode)) {
-    ESP_LOGE(TAG, "wifi_softap_set_dhcps_offer_option failed!");
+    ESP_LOGE(TAG, "wifi_softap_set_dhcps_offer_option failed");
     return false;
   }
 #endif
 
   if (!wifi_softap_dhcps_start()) {
-    ESP_LOGE(TAG, "Starting SoftAP DHCPS failed!");
+    ESP_LOGE(TAG, "Starting SoftAP DHCPS failed");
     return false;
   }
 
@@ -784,7 +784,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
 
   struct softap_config conf {};
   if (ap.get_ssid().size() > sizeof(conf.ssid)) {
-    ESP_LOGE(TAG, "AP SSID is too long");
+    ESP_LOGE(TAG, "AP SSID too long");
     return false;
   }
   memcpy(reinterpret_cast(conf.ssid), ap.get_ssid().c_str(), ap.get_ssid().size());
@@ -800,7 +800,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
   } else {
     conf.authmode = AUTH_WPA2_PSK;
     if (ap.get_password().size() > sizeof(conf.password)) {
-      ESP_LOGE(TAG, "AP password is too long");
+      ESP_LOGE(TAG, "AP password too long");
       return false;
     }
     memcpy(reinterpret_cast(conf.password), ap.get_password().c_str(), ap.get_password().size());
@@ -811,12 +811,12 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
   ETS_UART_INTR_ENABLE();
 
   if (!ret) {
-    ESP_LOGV(TAG, "wifi_softap_set_config_current failed!");
+    ESP_LOGV(TAG, "wifi_softap_set_config_current failed");
     return false;
   }
 
   if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) {
-    ESP_LOGV(TAG, "wifi_ap_ip_config_ failed!");
+    ESP_LOGV(TAG, "wifi_ap_ip_config_ failed");
     return false;
   }
 
diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp
index 1af271345f..f0655a6d1d 100644
--- a/esphome/components/wifi/wifi_component_esp_idf.cpp
+++ b/esphome/components/wifi/wifi_component_esp_idf.cpp
@@ -219,14 +219,14 @@ bool WiFiComponent::wifi_mode_(optional sta, optional ap) {
     return true;
 
   if (set_sta && !current_sta) {
-    ESP_LOGV(TAG, "Enabling STA.");
+    ESP_LOGV(TAG, "Enabling STA");
   } else if (!set_sta && current_sta) {
-    ESP_LOGV(TAG, "Disabling STA.");
+    ESP_LOGV(TAG, "Disabling STA");
   }
   if (set_ap && !current_ap) {
-    ESP_LOGV(TAG, "Enabling AP.");
+    ESP_LOGV(TAG, "Enabling AP");
   } else if (!set_ap && current_ap) {
-    ESP_LOGV(TAG, "Disabling AP.");
+    ESP_LOGV(TAG, "Disabling AP");
   }
 
   if (set_mode == WIFI_MODE_NULL && s_wifi_started) {
@@ -290,11 +290,11 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
   wifi_config_t conf;
   memset(&conf, 0, sizeof(conf));
   if (ap.get_ssid().size() > sizeof(conf.sta.ssid)) {
-    ESP_LOGE(TAG, "SSID is too long");
+    ESP_LOGE(TAG, "SSID too long");
     return false;
   }
   if (ap.get_password().size() > sizeof(conf.sta.password)) {
-    ESP_LOGE(TAG, "password is too long");
+    ESP_LOGE(TAG, "Password too long");
     return false;
   }
   memcpy(reinterpret_cast(conf.sta.ssid), ap.get_ssid().c_str(), ap.get_ssid().size());
@@ -490,7 +490,7 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) {
     if (dhcp_status != ESP_NETIF_DHCP_STARTED) {
       err = esp_netif_dhcpc_start(s_sta_netif);
       if (err != ESP_OK) {
-        ESP_LOGV(TAG, "Starting DHCP client failed! %d", err);
+        ESP_LOGV(TAG, "Starting DHCP client failed: %d", err);
       }
       return err == ESP_OK;
     }
@@ -503,12 +503,12 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) {
   info.netmask = manual_ip->subnet;
   err = esp_netif_dhcpc_stop(s_sta_netif);
   if (err != ESP_OK && err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STOPPED) {
-    ESP_LOGV(TAG, "Stopping DHCP client failed! %s", esp_err_to_name(err));
+    ESP_LOGV(TAG, "Stopping DHCP client failed: %s", esp_err_to_name(err));
   }
 
   err = esp_netif_set_ip_info(s_sta_netif, &info);
   if (err != ESP_OK) {
-    ESP_LOGV(TAG, "Setting manual IP info failed! %s", esp_err_to_name(err));
+    ESP_LOGV(TAG, "Setting manual IP info failed: %s", esp_err_to_name(err));
   }
 
   esp_netif_dns_info_t dns;
@@ -665,7 +665,7 @@ void WiFiComponent::wifi_loop_() {
 void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
   esp_err_t err;
   if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_START) {
-    ESP_LOGV(TAG, "Event: WiFi STA start");
+    ESP_LOGV(TAG, "STA start");
     // apply hostname
     err = esp_netif_set_hostname(s_sta_netif, App.get_name().c_str());
     if (err != ERR_OK) {
@@ -677,13 +677,12 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
     wifi_apply_power_save_();
 
   } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_STOP) {
-    ESP_LOGV(TAG, "Event: WiFi STA stop");
+    ESP_LOGV(TAG, "STA stop");
     s_sta_started = false;
 
   } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_AUTHMODE_CHANGE) {
     const auto &it = data->data.sta_authmode_change;
-    ESP_LOGV(TAG, "Event: Authmode Change old=%s new=%s", get_auth_mode_str(it.old_mode),
-             get_auth_mode_str(it.new_mode));
+    ESP_LOGV(TAG, "Authmode Change old=%s new=%s", get_auth_mode_str(it.old_mode), get_auth_mode_str(it.new_mode));
 
   } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_CONNECTED) {
     const auto &it = data->data.sta_connected;
@@ -691,8 +690,8 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
     assert(it.ssid_len <= 32);
     memcpy(buf, it.ssid, it.ssid_len);
     buf[it.ssid_len] = '\0';
-    ESP_LOGV(TAG, "Event: Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf,
-             format_mac_addr(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode));
+    ESP_LOGV(TAG, "Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf,
+             format_mac_address_pretty(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode));
     s_sta_connected = true;
 
   } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_DISCONNECTED) {
@@ -702,14 +701,14 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
     memcpy(buf, it.ssid, it.ssid_len);
     buf[it.ssid_len] = '\0';
     if (it.reason == WIFI_REASON_NO_AP_FOUND) {
-      ESP_LOGW(TAG, "Event: Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf);
+      ESP_LOGW(TAG, "Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf);
       s_sta_connect_not_found = true;
     } else if (it.reason == WIFI_REASON_ROAMING) {
-      ESP_LOGI(TAG, "Event: Disconnected ssid='%s' reason='Station Roaming'", buf);
+      ESP_LOGI(TAG, "Disconnected ssid='%s' reason='Station Roaming'", buf);
       return;
     } else {
-      ESP_LOGW(TAG, "Event: Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf,
-               format_mac_addr(it.bssid).c_str(), get_disconnect_reason_str(it.reason));
+      ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf,
+               format_mac_address_pretty(it.bssid).c_str(), get_disconnect_reason_str(it.reason));
       s_sta_connect_error = true;
     }
     s_sta_connected = false;
@@ -721,24 +720,24 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
 #if USE_NETWORK_IPV6
     esp_netif_create_ip6_linklocal(s_sta_netif);
 #endif /* USE_NETWORK_IPV6 */
-    ESP_LOGV(TAG, "Event: Got IP static_ip=%s gateway=%s", format_ip4_addr(it.ip_info.ip).c_str(),
+    ESP_LOGV(TAG, "static_ip=%s gateway=%s", format_ip4_addr(it.ip_info.ip).c_str(),
              format_ip4_addr(it.ip_info.gw).c_str());
     this->got_ipv4_address_ = true;
 
 #if USE_NETWORK_IPV6
   } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_GOT_IP6) {
     const auto &it = data->data.ip_got_ip6;
-    ESP_LOGV(TAG, "Event: Got IPv6 address=%s", format_ip6_addr(it.ip6_info.ip).c_str());
+    ESP_LOGV(TAG, "IPv6 address=%s", format_ip6_addr(it.ip6_info.ip).c_str());
     this->num_ipv6_addresses_++;
 #endif /* USE_NETWORK_IPV6 */
 
   } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_STA_LOST_IP) {
-    ESP_LOGV(TAG, "Event: Lost IP");
+    ESP_LOGV(TAG, "Lost IP");
     this->got_ipv4_address_ = false;
 
   } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_SCAN_DONE) {
     const auto &it = data->data.sta_scan_done;
-    ESP_LOGV(TAG, "Event: WiFi Scan Done status=%" PRIu32 " number=%u scan_id=%u", it.status, it.number, it.scan_id);
+    ESP_LOGV(TAG, "Scan done: status=%" PRIu32 " number=%u scan_id=%u", it.status, it.number, it.scan_id);
 
     scan_result_.clear();
     this->scan_done_ = true;
@@ -772,28 +771,28 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
     }
 
   } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_START) {
-    ESP_LOGV(TAG, "Event: WiFi AP start");
+    ESP_LOGV(TAG, "AP start");
     s_ap_started = true;
 
   } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_STOP) {
-    ESP_LOGV(TAG, "Event: WiFi AP stop");
+    ESP_LOGV(TAG, "AP stop");
     s_ap_started = false;
 
   } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_PROBEREQRECVED) {
     const auto &it = data->data.ap_probe_req_rx;
-    ESP_LOGVV(TAG, "Event: AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi);
+    ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", format_mac_address_pretty(it.mac).c_str(), it.rssi);
 
   } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_STACONNECTED) {
     const auto &it = data->data.ap_staconnected;
-    ESP_LOGV(TAG, "Event: AP client connected MAC=%s", format_mac_addr(it.mac).c_str());
+    ESP_LOGV(TAG, "AP client connected MAC=%s", format_mac_address_pretty(it.mac).c_str());
 
   } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_STADISCONNECTED) {
     const auto &it = data->data.ap_stadisconnected;
-    ESP_LOGV(TAG, "Event: AP client disconnected MAC=%s", format_mac_addr(it.mac).c_str());
+    ESP_LOGV(TAG, "AP client disconnected MAC=%s", format_mac_address_pretty(it.mac).c_str());
 
   } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_AP_STAIPASSIGNED) {
     const auto &it = data->data.ip_ap_staipassigned;
-    ESP_LOGV(TAG, "Event: AP client assigned IP %s", format_ip4_addr(it.ip).c_str());
+    ESP_LOGV(TAG, "AP client assigned IP %s", format_ip4_addr(it.ip).c_str());
   }
 }
 
@@ -873,7 +872,7 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) {
 
   err = esp_netif_set_ip_info(s_ap_netif, &info);
   if (err != ESP_OK) {
-    ESP_LOGE(TAG, "esp_netif_set_ip_info failed! %d", err);
+    ESP_LOGE(TAG, "esp_netif_set_ip_info failed: %d", err);
     return false;
   }
 
@@ -889,14 +888,14 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) {
   err = esp_netif_dhcps_option(s_ap_netif, ESP_NETIF_OP_SET, ESP_NETIF_REQUESTED_IP_ADDRESS, &lease, sizeof(lease));
 
   if (err != ESP_OK) {
-    ESP_LOGE(TAG, "esp_netif_dhcps_option failed! %d", err);
+    ESP_LOGE(TAG, "esp_netif_dhcps_option failed: %d", err);
     return false;
   }
 
   err = esp_netif_dhcps_start(s_ap_netif);
 
   if (err != ESP_OK) {
-    ESP_LOGE(TAG, "esp_netif_dhcps_start failed! %d", err);
+    ESP_LOGE(TAG, "esp_netif_dhcps_start failed: %d", err);
     return false;
   }
 
@@ -911,7 +910,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
   wifi_config_t conf;
   memset(&conf, 0, sizeof(conf));
   if (ap.get_ssid().size() > sizeof(conf.ap.ssid)) {
-    ESP_LOGE(TAG, "AP SSID is too long");
+    ESP_LOGE(TAG, "AP SSID too long");
     return false;
   }
   memcpy(reinterpret_cast(conf.ap.ssid), ap.get_ssid().c_str(), ap.get_ssid().size());
@@ -926,7 +925,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
   } else {
     conf.ap.authmode = WIFI_AUTH_WPA2_PSK;
     if (ap.get_password().size() > sizeof(conf.ap.password)) {
-      ESP_LOGE(TAG, "AP password is too long");
+      ESP_LOGE(TAG, "AP password too long");
       return false;
     }
     memcpy(reinterpret_cast(conf.ap.password), ap.get_password().c_str(), ap.get_password().size());
@@ -937,12 +936,12 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
 
   esp_err_t err = esp_wifi_set_config(WIFI_IF_AP, &conf);
   if (err != ESP_OK) {
-    ESP_LOGE(TAG, "esp_wifi_set_config failed! %d", err);
+    ESP_LOGE(TAG, "esp_wifi_set_config failed: %d", err);
     return false;
   }
 
   if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) {
-    ESP_LOGE(TAG, "wifi_ap_ip_config_ failed!");
+    ESP_LOGE(TAG, "wifi_ap_ip_config_ failed:");
     return false;
   }
 
diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp
index eb88ed81ad..b15f710150 100644
--- a/esphome/components/wifi/wifi_component_libretiny.cpp
+++ b/esphome/components/wifi/wifi_component_libretiny.cpp
@@ -32,14 +32,14 @@ bool WiFiComponent::wifi_mode_(optional sta, optional ap) {
     return true;
 
   if (enable_sta && !current_sta) {
-    ESP_LOGV(TAG, "Enabling STA.");
+    ESP_LOGV(TAG, "Enabling STA");
   } else if (!enable_sta && current_sta) {
-    ESP_LOGV(TAG, "Disabling STA.");
+    ESP_LOGV(TAG, "Disabling STA");
   }
   if (enable_ap && !current_ap) {
-    ESP_LOGV(TAG, "Enabling AP.");
+    ESP_LOGV(TAG, "Enabling AP");
   } else if (!enable_ap && current_ap) {
-    ESP_LOGV(TAG, "Disabling AP.");
+    ESP_LOGV(TAG, "Disabling AP");
   }
 
   uint8_t mode = 0;
@@ -124,7 +124,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
                                  ap.get_channel().has_value() ? *ap.get_channel() : 0,
                                  ap.get_bssid().has_value() ? ap.get_bssid()->data() : NULL);
   if (status != WL_CONNECTED) {
-    ESP_LOGW(TAG, "esp_wifi_connect failed! %d", status);
+    ESP_LOGW(TAG, "esp_wifi_connect failed: %d", status);
     return false;
   }
 
@@ -256,23 +256,23 @@ using esphome_wifi_event_info_t = arduino_event_info_t;
 void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_wifi_event_info_t info) {
   switch (event) {
     case ESPHOME_EVENT_ID_WIFI_READY: {
-      ESP_LOGV(TAG, "Event: WiFi ready");
+      ESP_LOGV(TAG, "Ready");
       break;
     }
     case ESPHOME_EVENT_ID_WIFI_SCAN_DONE: {
       auto it = info.wifi_scan_done;
-      ESP_LOGV(TAG, "Event: WiFi Scan Done status=%u number=%u scan_id=%u", it.status, it.number, it.scan_id);
+      ESP_LOGV(TAG, "Scan done: status=%u number=%u scan_id=%u", it.status, it.number, it.scan_id);
 
       this->wifi_scan_done_callback_();
       break;
     }
     case ESPHOME_EVENT_ID_WIFI_STA_START: {
-      ESP_LOGV(TAG, "Event: WiFi STA start");
+      ESP_LOGV(TAG, "STA start");
       WiFi.setHostname(App.get_name().c_str());
       break;
     }
     case ESPHOME_EVENT_ID_WIFI_STA_STOP: {
-      ESP_LOGV(TAG, "Event: WiFi STA stop");
+      ESP_LOGV(TAG, "STA stop");
       break;
     }
     case ESPHOME_EVENT_ID_WIFI_STA_CONNECTED: {
@@ -280,8 +280,8 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_
       char buf[33];
       memcpy(buf, it.ssid, it.ssid_len);
       buf[it.ssid_len] = '\0';
-      ESP_LOGV(TAG, "Event: Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf,
-               format_mac_addr(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode));
+      ESP_LOGV(TAG, "Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf,
+               format_mac_address_pretty(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode));
 
       break;
     }
@@ -291,10 +291,10 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_
       memcpy(buf, it.ssid, it.ssid_len);
       buf[it.ssid_len] = '\0';
       if (it.reason == WIFI_REASON_NO_AP_FOUND) {
-        ESP_LOGW(TAG, "Event: Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf);
+        ESP_LOGW(TAG, "Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf);
       } else {
-        ESP_LOGW(TAG, "Event: Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf,
-                 format_mac_addr(it.bssid).c_str(), get_disconnect_reason_str(it.reason));
+        ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf,
+                 format_mac_address_pretty(it.bssid).c_str(), get_disconnect_reason_str(it.reason));
       }
 
       uint8_t reason = it.reason;
@@ -310,8 +310,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_
     }
     case ESPHOME_EVENT_ID_WIFI_STA_AUTHMODE_CHANGE: {
       auto it = info.wifi_sta_authmode_change;
-      ESP_LOGV(TAG, "Event: Authmode Change old=%s new=%s", get_auth_mode_str(it.old_mode),
-               get_auth_mode_str(it.new_mode));
+      ESP_LOGV(TAG, "Authmode Change old=%s new=%s", get_auth_mode_str(it.old_mode), get_auth_mode_str(it.new_mode));
       // Mitigate CVE-2020-12638
       // https://lbsfilm.at/blog/wpa2-authenticationmode-downgrade-in-espressif-microprocessors
       if (it.old_mode != WIFI_AUTH_OPEN && it.new_mode == WIFI_AUTH_OPEN) {
@@ -325,47 +324,47 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_
     }
     case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP: {
       // auto it = info.got_ip.ip_info;
-      ESP_LOGV(TAG, "Event: Got IP static_ip=%s gateway=%s", format_ip4_addr(WiFi.localIP()).c_str(),
+      ESP_LOGV(TAG, "static_ip=%s gateway=%s", format_ip4_addr(WiFi.localIP()).c_str(),
                format_ip4_addr(WiFi.gatewayIP()).c_str());
       s_sta_connecting = false;
       break;
     }
     case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP6: {
       // auto it = info.got_ip.ip_info;
-      ESP_LOGV(TAG, "Event: Got IPv6");
+      ESP_LOGV(TAG, "Got IPv6");
       break;
     }
     case ESPHOME_EVENT_ID_WIFI_STA_LOST_IP: {
-      ESP_LOGV(TAG, "Event: Lost IP");
+      ESP_LOGV(TAG, "Lost IP");
       break;
     }
     case ESPHOME_EVENT_ID_WIFI_AP_START: {
-      ESP_LOGV(TAG, "Event: WiFi AP start");
+      ESP_LOGV(TAG, "AP start");
       break;
     }
     case ESPHOME_EVENT_ID_WIFI_AP_STOP: {
-      ESP_LOGV(TAG, "Event: WiFi AP stop");
+      ESP_LOGV(TAG, "AP stop");
       break;
     }
     case ESPHOME_EVENT_ID_WIFI_AP_STACONNECTED: {
       auto it = info.wifi_sta_connected;
       auto &mac = it.bssid;
-      ESP_LOGV(TAG, "Event: AP client connected MAC=%s", format_mac_addr(mac).c_str());
+      ESP_LOGV(TAG, "AP client connected MAC=%s", format_mac_address_pretty(mac).c_str());
       break;
     }
     case ESPHOME_EVENT_ID_WIFI_AP_STADISCONNECTED: {
       auto it = info.wifi_sta_disconnected;
       auto &mac = it.bssid;
-      ESP_LOGV(TAG, "Event: AP client disconnected MAC=%s", format_mac_addr(mac).c_str());
+      ESP_LOGV(TAG, "AP client disconnected MAC=%s", format_mac_address_pretty(mac).c_str());
       break;
     }
     case ESPHOME_EVENT_ID_WIFI_AP_STAIPASSIGNED: {
-      ESP_LOGV(TAG, "Event: AP client assigned IP");
+      ESP_LOGV(TAG, "AP client assigned IP");
       break;
     }
     case ESPHOME_EVENT_ID_WIFI_AP_PROBEREQRECVED: {
       auto it = info.wifi_ap_probereqrecved;
-      ESP_LOGVV(TAG, "Event: AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi);
+      ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", format_mac_address_pretty(it.mac).c_str(), it.rssi);
       break;
     }
     default:
@@ -399,7 +398,7 @@ bool WiFiComponent::wifi_scan_start_(bool passive) {
   // need to use WiFi because of WiFiScanClass allocations :(
   int16_t err = WiFi.scanNetworks(true, true, passive, 200);
   if (err != WIFI_SCAN_RUNNING) {
-    ESP_LOGV(TAG, "WiFi.scanNetworks failed! %d", err);
+    ESP_LOGV(TAG, "WiFi.scanNetworks failed: %d", err);
     return false;
   }
 
@@ -447,7 +446,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
     return false;
 
   if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) {
-    ESP_LOGV(TAG, "wifi_ap_ip_config_ failed!");
+    ESP_LOGV(TAG, "wifi_ap_ip_config_ failed");
     return false;
   }
 
diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp
index 23fd766abe..bf15892cd5 100644
--- a/esphome/components/wifi/wifi_component_pico_w.cpp
+++ b/esphome/components/wifi/wifi_component_pico_w.cpp
@@ -134,7 +134,7 @@ bool WiFiComponent::wifi_scan_start_(bool passive) {
   scan_options.scan_type = passive ? 1 : 0;
   int err = cyw43_wifi_scan(&cyw43_state, &scan_options, nullptr, &s_wifi_scan_result);
   if (err) {
-    ESP_LOGV(TAG, "cyw43_wifi_scan failed!");
+    ESP_LOGV(TAG, "cyw43_wifi_scan failed");
   }
   return err == 0;
   return true;
@@ -162,7 +162,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
   if (!this->wifi_mode_({}, true))
     return false;
   if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) {
-    ESP_LOGV(TAG, "wifi_ap_ip_config_ failed!");
+    ESP_LOGV(TAG, "wifi_ap_ip_config_ failed");
     return false;
   }
 
@@ -209,7 +209,7 @@ network::IPAddress WiFiComponent::wifi_dns_ip_(int num) {
 void WiFiComponent::wifi_loop_() {
   if (this->state_ == WIFI_COMPONENT_STATE_STA_SCANNING && !cyw43_wifi_scan_active(&cyw43_state)) {
     this->scan_done_ = true;
-    ESP_LOGV(TAG, "Scan done!");
+    ESP_LOGV(TAG, "Scan done");
   }
 }
 
diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.cpp b/esphome/components/wifi_info/wifi_info_text_sensor.cpp
index 150c7229f8..2612e4af8d 100644
--- a/esphome/components/wifi_info/wifi_info_text_sensor.cpp
+++ b/esphome/components/wifi_info/wifi_info_text_sensor.cpp
@@ -7,12 +7,12 @@ namespace wifi_info {
 
 static const char *const TAG = "wifi_info";
 
-void IPAddressWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo IPAddress", this); }
-void ScanResultsWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo Scan Results", this); }
-void SSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo SSID", this); }
-void BSSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo BSSID", this); }
-void MacAddressWifiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo Mac Address", this); }
-void DNSAddressWifiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo DNS Address", this); }
+void IPAddressWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "IP Address", this); }
+void ScanResultsWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "Scan Results", this); }
+void SSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "SSID", this); }
+void BSSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "BSSID", this); }
+void MacAddressWifiInfo::dump_config() { LOG_TEXT_SENSOR("", "MAC Address", this); }
+void DNSAddressWifiInfo::dump_config() { LOG_TEXT_SENSOR("", "DNS Address", this); }
 
 }  // namespace wifi_info
 }  // namespace esphome
diff --git a/esphome/components/wireguard/__init__.py b/esphome/components/wireguard/__init__.py
index fc0e4e0538..8eff8e7b2a 100644
--- a/esphome/components/wireguard/__init__.py
+++ b/esphome/components/wireguard/__init__.py
@@ -6,14 +6,7 @@ import esphome.codegen as cg
 from esphome.components import time
 from esphome.components.esp32 import CORE, add_idf_sdkconfig_option
 import esphome.config_validation as cv
-from esphome.const import (
-    CONF_ADDRESS,
-    CONF_ID,
-    CONF_REBOOT_TIMEOUT,
-    CONF_TIME_ID,
-    KEY_CORE,
-    KEY_FRAMEWORK_VERSION,
-)
+from esphome.const import CONF_ADDRESS, CONF_ID, CONF_REBOOT_TIMEOUT, CONF_TIME_ID
 from esphome.core import TimePeriod
 
 CONF_NETMASK = "netmask"
@@ -125,9 +118,7 @@ async def to_code(config):
 
     # Workaround for crash on IDF 5+
     # See https://github.com/trombik/esp_wireguard/issues/33#issuecomment-1568503651
-    if CORE.using_esp_idf and CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] >= cv.Version(
-        5, 0, 0
-    ):
+    if CORE.using_esp_idf:
         add_idf_sdkconfig_option("CONFIG_LWIP_PPP_SUPPORT", True)
 
     # This flag is added here because the esp_wireguard library statically
diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.cpp b/esphome/components/xiaomi_ble/xiaomi_ble.cpp
index 04e0724ba7..564870d74e 100644
--- a/esphome/components/xiaomi_ble/xiaomi_ble.cpp
+++ b/esphome/components/xiaomi_ble/xiaomi_ble.cpp
@@ -91,6 +91,13 @@ bool parse_xiaomi_value(uint16_t value_type, const uint8_t *data, uint8_t value_
   // MiaoMiaoce humidity, 1 byte, 8-bit unsigned integer, 1 %
   else if ((value_type == 0x4C02) && (value_length == 1)) {
     result.humidity = data[0];
+  }
+  // XMWSDJ04MMC humidity, 4 bytes, float, 0.1 °C
+  else if ((value_type == 0x4C08) && (value_length == 4)) {
+    const uint32_t int_number = encode_uint32(data[3], data[2], data[1], data[0]);
+    float humidity;
+    std::memcpy(&humidity, &int_number, sizeof(humidity));
+    result.humidity = humidity;
   } else {
     return false;
   }
@@ -219,6 +226,11 @@ optional parse_xiaomi_header(const esp32_ble_tracker::Service
   } else if (device_uuid == 0x055b) {  // small square body, segment LCD, encrypted
     result.type = XiaomiParseResult::TYPE_LYWSD03MMC;
     result.name = "LYWSD03MMC";
+  } else if (device_uuid == 0x1203) {  // small square body, e-ink display, encrypted
+    result.type = XiaomiParseResult::TYPE_XMWSDJ04MMC;
+    result.name = "XMWSDJ04MMC";
+    if (raw.size() == 19)
+      result.raw_offset -= 6;
   } else if (device_uuid == 0x07f6) {  // Xiaomi-Yeelight BLE nightlight
     result.type = XiaomiParseResult::TYPE_MJYD02YLA;
     result.name = "MJYD02YLA";
@@ -308,7 +320,7 @@ bool decrypt_xiaomi_payload(std::vector &raw, const uint8_t *bindkey, c
     memcpy(mac_address + 4, mac_reverse + 1, 1);
     memcpy(mac_address + 5, mac_reverse, 1);
     ESP_LOGVV(TAG, "decrypt_xiaomi_payload(): authenticated decryption failed.");
-    ESP_LOGVV(TAG, "  MAC address : %s", format_hex_pretty(mac_address, 6).c_str());
+    ESP_LOGVV(TAG, "  MAC address : %s", format_mac_address_pretty(mac_address).c_str());
     ESP_LOGVV(TAG, "       Packet : %s", format_hex_pretty(raw.data(), raw.size()).c_str());
     ESP_LOGVV(TAG, "          Key : %s", format_hex_pretty(vector.key, vector.keysize).c_str());
     ESP_LOGVV(TAG, "           Iv : %s", format_hex_pretty(vector.iv, vector.ivsize).c_str());
diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.h b/esphome/components/xiaomi_ble/xiaomi_ble.h
index 6978be97f4..77fb04fd78 100644
--- a/esphome/components/xiaomi_ble/xiaomi_ble.h
+++ b/esphome/components/xiaomi_ble/xiaomi_ble.h
@@ -20,6 +20,7 @@ struct XiaomiParseResult {
     TYPE_LYWSD02MMC,
     TYPE_CGG1,
     TYPE_LYWSD03MMC,
+    TYPE_XMWSDJ04MMC,
     TYPE_CGD1,
     TYPE_CGDK2,
     TYPE_JQJCY01YM,
diff --git a/esphome/components/xiaomi_cgd1/xiaomi_cgd1.h b/esphome/components/xiaomi_cgd1/xiaomi_cgd1.h
index d05cffc4d1..393795439b 100644
--- a/esphome/components/xiaomi_cgd1/xiaomi_cgd1.h
+++ b/esphome/components/xiaomi_cgd1/xiaomi_cgd1.h
@@ -17,7 +17,6 @@ class XiaomiCGD1 : public Component, public esp32_ble_tracker::ESPBTDeviceListen
 
   bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
   void dump_config() override;
-  float get_setup_priority() const override { return setup_priority::DATA; }
   void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; }
   void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; }
   void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; }
diff --git a/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.h b/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.h
index 8fd9946537..1f5ef89869 100644
--- a/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.h
+++ b/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.h
@@ -17,7 +17,6 @@ class XiaomiCGDK2 : public Component, public esp32_ble_tracker::ESPBTDeviceListe
 
   bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
   void dump_config() override;
-  float get_setup_priority() const override { return setup_priority::DATA; }
   void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; }
   void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; }
   void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; }
diff --git a/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h
index 966c05ac79..52904fd75e 100644
--- a/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h
+++ b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h
@@ -18,7 +18,6 @@ class XiaomiCGG1 : public Component, public esp32_ble_tracker::ESPBTDeviceListen
   bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
 
   void dump_config() override;
-  float get_setup_priority() const override { return setup_priority::DATA; }
   void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; }
   void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; }
   void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; }
diff --git a/esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.h b/esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.h
index eff4b1c6fb..124f9411a1 100644
--- a/esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.h
+++ b/esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.h
@@ -21,7 +21,6 @@ class XiaomiCGPR1 : public Component,
   bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
 
   void dump_config() override;
-  float get_setup_priority() const override { return setup_priority::DATA; }
   void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; }
   void set_illuminance(sensor::Sensor *illuminance) { illuminance_ = illuminance; }
   void set_idle_time(sensor::Sensor *idle_time) { idle_time_ = idle_time; }
diff --git a/esphome/components/xiaomi_gcls002/xiaomi_gcls002.h b/esphome/components/xiaomi_gcls002/xiaomi_gcls002.h
index 08e1bd7e54..83c8f15ace 100644
--- a/esphome/components/xiaomi_gcls002/xiaomi_gcls002.h
+++ b/esphome/components/xiaomi_gcls002/xiaomi_gcls002.h
@@ -17,7 +17,6 @@ class XiaomiGCLS002 : public Component, public esp32_ble_tracker::ESPBTDeviceLis
   bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
 
   void dump_config() override;
-  float get_setup_priority() const override { return setup_priority::DATA; }
   void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; }
   void set_moisture(sensor::Sensor *moisture) { moisture_ = moisture; }
   void set_conductivity(sensor::Sensor *conductivity) { conductivity_ = conductivity; }
diff --git a/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h b/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h
index aa99cc004a..96ea9217fb 100644
--- a/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h
+++ b/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h
@@ -17,7 +17,6 @@ class XiaomiHHCCJCY01 : public Component, public esp32_ble_tracker::ESPBTDeviceL
   bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
 
   void dump_config() override;
-  float get_setup_priority() const override { return setup_priority::DATA; }
   void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; }
   void set_moisture(sensor::Sensor *moisture) { moisture_ = moisture; }
   void set_conductivity(sensor::Sensor *conductivity) { conductivity_ = conductivity; }
diff --git a/esphome/components/xiaomi_hhccjcy10/xiaomi_hhccjcy10.h b/esphome/components/xiaomi_hhccjcy10/xiaomi_hhccjcy10.h
index bc1e580ce4..bd4ad75c1d 100644
--- a/esphome/components/xiaomi_hhccjcy10/xiaomi_hhccjcy10.h
+++ b/esphome/components/xiaomi_hhccjcy10/xiaomi_hhccjcy10.h
@@ -16,7 +16,6 @@ class XiaomiHHCCJCY10 : public Component, public esp32_ble_tracker::ESPBTDeviceL
   bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
 
   void dump_config() override;
-  float get_setup_priority() const override { return setup_priority::DATA; }
   void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; }
   void set_moisture(sensor::Sensor *moisture) { this->moisture_ = moisture; }
   void set_conductivity(sensor::Sensor *conductivity) { this->conductivity_ = conductivity; }
diff --git a/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.h b/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.h
index ce746b9ee0..0ec34b1871 100644
--- a/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.h
+++ b/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.h
@@ -17,7 +17,6 @@ class XiaomiHHCCPOT002 : public Component, public esp32_ble_tracker::ESPBTDevice
   bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
 
   void dump_config() override;
-  float get_setup_priority() const override { return setup_priority::DATA; }
   void set_moisture(sensor::Sensor *moisture) { moisture_ = moisture; }
   void set_conductivity(sensor::Sensor *conductivity) { conductivity_ = conductivity; }
 
diff --git a/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.h b/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.h
index ca1ad0f27e..e9c44800f2 100644
--- a/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.h
+++ b/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.h
@@ -17,7 +17,6 @@ class XiaomiJQJCY01YM : public Component, public esp32_ble_tracker::ESPBTDeviceL
   bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
 
   void dump_config() override;
-  float get_setup_priority() const override { return setup_priority::DATA; }
   void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; }
   void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; }
   void set_formaldehyde(sensor::Sensor *formaldehyde) { formaldehyde_ = formaldehyde; }
diff --git a/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h
index 641a02bd5a..772b389a92 100644
--- a/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h
+++ b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h
@@ -17,7 +17,6 @@ class XiaomiLYWSD02 : public Component, public esp32_ble_tracker::ESPBTDeviceLis
   bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
 
   void dump_config() override;
-  float get_setup_priority() const override { return setup_priority::DATA; }
   void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; }
   void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; }
   void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; }
diff --git a/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.h b/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.h
index 19092aa2a9..e1e0fcae40 100644
--- a/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.h
+++ b/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.h
@@ -18,7 +18,6 @@ class XiaomiLYWSD02MMC : public Component, public esp32_ble_tracker::ESPBTDevice
   bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
 
   void dump_config() override;
-  float get_setup_priority() const override { return setup_priority::DATA; }
   void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; }
   void set_humidity(sensor::Sensor *humidity) { this->humidity_ = humidity; }
   void set_battery_level(sensor::Sensor *battery_level) { this->battery_level_ = battery_level; }
diff --git a/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.h b/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.h
index 95710a1508..3c7907479a 100644
--- a/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.h
+++ b/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.h
@@ -17,7 +17,6 @@ class XiaomiLYWSD03MMC : public Component, public esp32_ble_tracker::ESPBTDevice
 
   bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
   void dump_config() override;
-  float get_setup_priority() const override { return setup_priority::DATA; }
   void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; }
   void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; }
   void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; }
diff --git a/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h b/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h
index cbc76f9dd3..cf90db937f 100644
--- a/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h
+++ b/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h
@@ -17,7 +17,6 @@ class XiaomiLYWSDCGQ : public Component, public esp32_ble_tracker::ESPBTDeviceLi
   bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
 
   void dump_config() override;
-  float get_setup_priority() const override { return setup_priority::DATA; }
   void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; }
   void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; }
   void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; }
diff --git a/esphome/components/xiaomi_mhoc303/xiaomi_mhoc303.h b/esphome/components/xiaomi_mhoc303/xiaomi_mhoc303.h
index d0304f7894..c3b8e7d68f 100644
--- a/esphome/components/xiaomi_mhoc303/xiaomi_mhoc303.h
+++ b/esphome/components/xiaomi_mhoc303/xiaomi_mhoc303.h
@@ -17,7 +17,6 @@ class XiaomiMHOC303 : public Component, public esp32_ble_tracker::ESPBTDeviceLis
   bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
 
   void dump_config() override;
-  float get_setup_priority() const override { return setup_priority::DATA; }
   void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; }
   void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; }
   void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; }
diff --git a/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.h b/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.h
index 4ab882b2af..1acdaa88af 100644
--- a/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.h
+++ b/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.h
@@ -17,7 +17,6 @@ class XiaomiMHOC401 : public Component, public esp32_ble_tracker::ESPBTDeviceLis
 
   bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
   void dump_config() override;
-  float get_setup_priority() const override { return setup_priority::DATA; }
   void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; }
   void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; }
   void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; }
diff --git a/esphome/components/xiaomi_miscale/xiaomi_miscale.h b/esphome/components/xiaomi_miscale/xiaomi_miscale.h
index 4523bbc82b..10d308ef6c 100644
--- a/esphome/components/xiaomi_miscale/xiaomi_miscale.h
+++ b/esphome/components/xiaomi_miscale/xiaomi_miscale.h
@@ -23,7 +23,6 @@ class XiaomiMiscale : public Component, public esp32_ble_tracker::ESPBTDeviceLis
 
   bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
   void dump_config() override;
-  float get_setup_priority() const override { return setup_priority::DATA; }
   void set_weight(sensor::Sensor *weight) { weight_ = weight; }
   void set_impedance(sensor::Sensor *impedance) { impedance_ = impedance; }
   void set_clear_impedance(bool clear_impedance) { clear_impedance_ = clear_impedance; }
diff --git a/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h b/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h
index 34b1fe4af0..e1b4055696 100644
--- a/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h
+++ b/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h
@@ -21,7 +21,6 @@ class XiaomiMJYD02YLA : public Component,
   bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
 
   void dump_config() override;
-  float get_setup_priority() const override { return setup_priority::DATA; }
   void set_idle_time(sensor::Sensor *idle_time) { idle_time_ = idle_time; }
   void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; }
   void set_illuminance(sensor::Sensor *illuminance) { illuminance_ = illuminance; }
diff --git a/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.h b/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.h
index 904c575ae6..f1da0705d0 100644
--- a/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.h
+++ b/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.h
@@ -19,7 +19,6 @@ class XiaomiMUE4094RT : public Component,
   bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
 
   void dump_config() override;
-  float get_setup_priority() const override { return setup_priority::DATA; }
   void set_time(uint16_t timeout) { timeout_ = timeout; }
 
  protected:
diff --git a/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.h b/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.h
index a16c5209d9..ae00a28ac9 100644
--- a/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.h
+++ b/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.h
@@ -23,7 +23,6 @@ class XiaomiRTCGQ02LM : public Component, public esp32_ble_tracker::ESPBTDeviceL
 
   bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
   void dump_config() override;
-  float get_setup_priority() const override { return setup_priority::DATA; }
 
 #ifdef USE_BINARY_SENSOR
   void set_motion(binary_sensor::BinarySensor *motion) { this->motion_ = motion; }
diff --git a/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.h b/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.h
index 297c7ab47d..081705fd50 100644
--- a/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.h
+++ b/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.h
@@ -20,7 +20,6 @@ class XiaomiWX08ZM : public Component,
   bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
 
   void dump_config() override;
-  float get_setup_priority() const override { return setup_priority::DATA; }
   void set_tablet(sensor::Sensor *tablet) { tablet_ = tablet; }
   void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; }
 
diff --git a/esphome/components/xiaomi_xmwsdj04mmc/__init__.py b/esphome/components/xiaomi_xmwsdj04mmc/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/xiaomi_xmwsdj04mmc/sensor.py b/esphome/components/xiaomi_xmwsdj04mmc/sensor.py
new file mode 100644
index 0000000000..b41a775f35
--- /dev/null
+++ b/esphome/components/xiaomi_xmwsdj04mmc/sensor.py
@@ -0,0 +1,77 @@
+import esphome.codegen as cg
+from esphome.components import esp32_ble_tracker, sensor
+import esphome.config_validation as cv
+from esphome.const import (
+    CONF_BATTERY_LEVEL,
+    CONF_BINDKEY,
+    CONF_HUMIDITY,
+    CONF_ID,
+    CONF_MAC_ADDRESS,
+    CONF_TEMPERATURE,
+    DEVICE_CLASS_BATTERY,
+    DEVICE_CLASS_HUMIDITY,
+    DEVICE_CLASS_TEMPERATURE,
+    ENTITY_CATEGORY_DIAGNOSTIC,
+    STATE_CLASS_MEASUREMENT,
+    UNIT_CELSIUS,
+    UNIT_PERCENT,
+)
+
+AUTO_LOAD = ["xiaomi_ble"]
+CODEOWNERS = ["@medusalix"]
+DEPENDENCIES = ["esp32_ble_tracker"]
+
+xiaomi_xmwsdj04mmc_ns = cg.esphome_ns.namespace("xiaomi_xmwsdj04mmc")
+XiaomiXMWSDJ04MMC = xiaomi_xmwsdj04mmc_ns.class_(
+    "XiaomiXMWSDJ04MMC", esp32_ble_tracker.ESPBTDeviceListener, cg.Component
+)
+
+CONFIG_SCHEMA = (
+    cv.Schema(
+        {
+            cv.GenerateID(): cv.declare_id(XiaomiXMWSDJ04MMC),
+            cv.Required(CONF_BINDKEY): cv.bind_key,
+            cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
+            cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
+                unit_of_measurement=UNIT_CELSIUS,
+                accuracy_decimals=1,
+                device_class=DEVICE_CLASS_TEMPERATURE,
+                state_class=STATE_CLASS_MEASUREMENT,
+            ),
+            cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
+                unit_of_measurement=UNIT_PERCENT,
+                accuracy_decimals=0,
+                device_class=DEVICE_CLASS_HUMIDITY,
+                state_class=STATE_CLASS_MEASUREMENT,
+            ),
+            cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(
+                unit_of_measurement=UNIT_PERCENT,
+                accuracy_decimals=0,
+                device_class=DEVICE_CLASS_BATTERY,
+                state_class=STATE_CLASS_MEASUREMENT,
+                entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+            ),
+        }
+    )
+    .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
+    .extend(cv.COMPONENT_SCHEMA)
+)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
+    await esp32_ble_tracker.register_ble_device(var, config)
+
+    cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
+    cg.add(var.set_bindkey(config[CONF_BINDKEY]))
+
+    if temperature_config := config.get(CONF_TEMPERATURE):
+        sens = await sensor.new_sensor(temperature_config)
+        cg.add(var.set_temperature(sens))
+    if humidity_config := config.get(CONF_HUMIDITY):
+        sens = await sensor.new_sensor(humidity_config)
+        cg.add(var.set_humidity(sens))
+    if battery_level_config := config.get(CONF_BATTERY_LEVEL):
+        sens = await sensor.new_sensor(battery_level_config)
+        cg.add(var.set_battery_level(sens))
diff --git a/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.cpp b/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.cpp
new file mode 100644
index 0000000000..f8712e7fd4
--- /dev/null
+++ b/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.cpp
@@ -0,0 +1,77 @@
+#include "xiaomi_xmwsdj04mmc.h"
+#include "esphome/core/log.h"
+
+#ifdef USE_ESP32
+
+namespace esphome {
+namespace xiaomi_xmwsdj04mmc {
+
+static const char *const TAG = "xiaomi_xmwsdj04mmc";
+
+void XiaomiXMWSDJ04MMC::dump_config() {
+  ESP_LOGCONFIG(TAG, "Xiaomi XMWSDJ04MMC");
+  ESP_LOGCONFIG(TAG, "  Bindkey: %s", format_hex_pretty(this->bindkey_, 16).c_str());
+  LOG_SENSOR("  ", "Temperature", this->temperature_);
+  LOG_SENSOR("  ", "Humidity", this->humidity_);
+  LOG_SENSOR("  ", "Battery Level", this->battery_level_);
+}
+
+bool XiaomiXMWSDJ04MMC::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
+  if (device.address_uint64() != this->address_) {
+    ESP_LOGVV(TAG, "parse_device(): unknown MAC address.");
+    return false;
+  }
+  ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str());
+
+  bool success = false;
+  for (auto &service_data : device.get_service_datas()) {
+    auto res = xiaomi_ble::parse_xiaomi_header(service_data);
+    if (!res.has_value()) {
+      continue;
+    }
+    if (res->is_duplicate) {
+      continue;
+    }
+    if (res->has_encryption &&
+        (!(xiaomi_ble::decrypt_xiaomi_payload(const_cast &>(service_data.data), this->bindkey_,
+                                              this->address_)))) {
+      continue;
+    }
+    if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) {
+      continue;
+    }
+    if (res->humidity.has_value() && this->humidity_ != nullptr) {
+      // see https://github.com/custom-components/sensor.mitemp_bt/issues/7#issuecomment-595948254
+      *res->humidity = trunc(*res->humidity);
+    }
+    if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) {
+      continue;
+    }
+    if (res->temperature.has_value() && this->temperature_ != nullptr)
+      this->temperature_->publish_state(*res->temperature);
+    if (res->humidity.has_value() && this->humidity_ != nullptr)
+      this->humidity_->publish_state(*res->humidity);
+    if (res->battery_level.has_value() && this->battery_level_ != nullptr)
+      this->battery_level_->publish_state(*res->battery_level);
+    success = true;
+  }
+
+  return success;
+}
+
+void XiaomiXMWSDJ04MMC::set_bindkey(const std::string &bindkey) {
+  memset(this->bindkey_, 0, 16);
+  if (bindkey.size() != 32) {
+    return;
+  }
+  char temp[3] = {0};
+  for (int i = 0; i < 16; i++) {
+    strncpy(temp, &(bindkey.c_str()[i * 2]), 2);
+    this->bindkey_[i] = std::strtoul(temp, nullptr, 16);
+  }
+}
+
+}  // namespace xiaomi_xmwsdj04mmc
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.h b/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.h
new file mode 100644
index 0000000000..ed0458ce49
--- /dev/null
+++ b/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.h
@@ -0,0 +1,36 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/sensor/sensor.h"
+#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
+#include "esphome/components/xiaomi_ble/xiaomi_ble.h"
+
+#ifdef USE_ESP32
+
+namespace esphome {
+namespace xiaomi_xmwsdj04mmc {
+
+class XiaomiXMWSDJ04MMC : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
+ public:
+  void set_address(uint64_t address) { this->address_ = address; }
+  void set_bindkey(const std::string &bindkey);
+
+  bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
+
+  void dump_config() override;
+  void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; }
+  void set_humidity(sensor::Sensor *humidity) { this->humidity_ = humidity; }
+  void set_battery_level(sensor::Sensor *battery_level) { this->battery_level_ = battery_level; }
+
+ protected:
+  uint64_t address_;
+  uint8_t bindkey_[16];
+  sensor::Sensor *temperature_{nullptr};
+  sensor::Sensor *humidity_{nullptr};
+  sensor::Sensor *battery_level_{nullptr};
+};
+
+}  // namespace xiaomi_xmwsdj04mmc
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/zio_ultrasonic/zio_ultrasonic.h b/esphome/components/zio_ultrasonic/zio_ultrasonic.h
index 84c8d44c65..23057b2ab0 100644
--- a/esphome/components/zio_ultrasonic/zio_ultrasonic.h
+++ b/esphome/components/zio_ultrasonic/zio_ultrasonic.h
@@ -11,8 +11,6 @@ namespace zio_ultrasonic {
 
 class ZioUltrasonicComponent : public i2c::I2CDevice, public PollingComponent, public sensor::Sensor {
  public:
-  float get_setup_priority() const override { return setup_priority::DATA; }
-
   void dump_config() override;
 
   void update() override;
diff --git a/esphome/components/zyaura/zyaura.h b/esphome/components/zyaura/zyaura.h
index 85c31ec75a..3070aa90c5 100644
--- a/esphome/components/zyaura/zyaura.h
+++ b/esphome/components/zyaura/zyaura.h
@@ -69,7 +69,6 @@ class ZyAuraSensor : public PollingComponent {
   void setup() override { this->store_.setup(this->pin_clock_, this->pin_data_); }
   void dump_config() override;
   void update() override;
-  float get_setup_priority() const override { return setup_priority::DATA; }
 
  protected:
   ZaSensorStore store_;
diff --git a/esphome/config.py b/esphome/config.py
index ca3686a0e6..c4aa9aea24 100644
--- a/esphome/config.py
+++ b/esphome/config.py
@@ -67,6 +67,42 @@ ConfigPath = list[str | int]
 path_context = contextvars.ContextVar("Config path")
 
 
+def _process_platform_config(
+    result: Config,
+    component_name: str,
+    platform_name: str,
+    platform_config: ConfigType,
+    path: ConfigPath,
+) -> None:
+    """Process a platform configuration and add necessary validation steps.
+
+    This is shared between LoadValidationStep and AutoLoadValidationStep to avoid duplication.
+    """
+    # Get the platform manifest
+    platform = get_platform(component_name, platform_name)
+    if platform is None:
+        result.add_str_error(
+            f"Platform not found: '{component_name}.{platform_name}'", path
+        )
+        return
+
+    # Add platform to loaded integrations
+    CORE.loaded_integrations.add(platform_name)
+    CORE.loaded_platforms.add(f"{component_name}/{platform_name}")
+
+    # Process platform's AUTO_LOAD
+    for load in platform.auto_load:
+        if load not in result:
+            result.add_validation_step(AutoLoadValidationStep(load))
+
+    # Add validation steps for the platform
+    p_domain = f"{component_name}.{platform_name}"
+    result.add_output_path(path, p_domain)
+    result.add_validation_step(
+        MetadataValidationStep(path, p_domain, platform_config, platform)
+    )
+
+
 def _path_begins_with(path: ConfigPath, other: ConfigPath) -> bool:
     if len(path) < len(other):
         return False
@@ -379,26 +415,11 @@ class LoadValidationStep(ConfigValidationStep):
                     path,
                 )
                 continue
-            # Remove temp output path and construct new one
+            # Remove temp output path
             result.remove_output_path(path, p_domain)
-            p_domain = f"{self.domain}.{p_name}"
-            result.add_output_path(path, p_domain)
-            # Try Load platform
-            platform = get_platform(self.domain, p_name)
-            if platform is None:
-                result.add_str_error(f"Platform not found: '{p_domain}'", path)
-                continue
-            CORE.loaded_integrations.add(p_name)
-            CORE.loaded_platforms.add(f"{self.domain}/{p_name}")
 
-            # Process AUTO_LOAD
-            for load in platform.auto_load:
-                if load not in result:
-                    result.add_validation_step(AutoLoadValidationStep(load))
-
-            result.add_validation_step(
-                MetadataValidationStep(path, p_domain, p_config, platform)
-            )
+            # Process the platform configuration
+            _process_platform_config(result, self.domain, p_name, p_config, path)
 
 
 class AutoLoadValidationStep(ConfigValidationStep):
@@ -413,10 +434,56 @@ class AutoLoadValidationStep(ConfigValidationStep):
         self.domain = domain
 
     def run(self, result: Config) -> None:
-        if self.domain in result:
-            # already loaded
+        # Regular component auto-load (no platform)
+        if "." not in self.domain:
+            if self.domain in result:
+                # already loaded
+                return
+            result.add_validation_step(LoadValidationStep(self.domain, core.AutoLoad()))
             return
-        result.add_validation_step(LoadValidationStep(self.domain, core.AutoLoad()))
+
+        # Platform-specific auto-load (e.g., "ota.web_server")
+        component_name, _, platform_name = self.domain.partition(".")
+
+        # Check if component exists
+        if component_name not in result:
+            # Component doesn't exist, load it first
+            result.add_validation_step(LoadValidationStep(component_name, []))
+            # Re-run this step after the component is loaded
+            result.add_validation_step(AutoLoadValidationStep(self.domain))
+            return
+
+        # Component exists, check if it's a platform component
+        component = get_component(component_name)
+        if component is None or not component.is_platform_component:
+            result.add_str_error(
+                f"Component {component_name} is not a platform component, "
+                f"cannot auto-load platform {platform_name}",
+                [component_name],
+            )
+            return
+
+        # Ensure the component config is a list
+        component_conf = result.get(component_name)
+        if not isinstance(component_conf, list):
+            component_conf = result[component_name] = []
+
+        # Check if platform already exists
+        if any(
+            isinstance(conf, dict) and conf.get(CONF_PLATFORM) == platform_name
+            for conf in component_conf
+        ):
+            return
+
+        # Add and process the platform configuration
+        platform_conf = core.AutoLoad()
+        platform_conf[CONF_PLATFORM] = platform_name
+        component_conf.append(platform_conf)
+
+        path = [component_name, len(component_conf) - 1]
+        _process_platform_config(
+            result, component_name, platform_name, platform_conf, path
+        )
 
 
 class MetadataValidationStep(ConfigValidationStep):
@@ -789,7 +856,6 @@ def validate_config(
         result.add_output_path([CONF_SUBSTITUTIONS], CONF_SUBSTITUTIONS)
         try:
             substitutions.do_substitution_pass(config, command_line_substitutions)
-            substitutions.do_substitution_pass(config, command_line_substitutions)
         except vol.Invalid as err:
             result.add_error(err)
             return result
diff --git a/esphome/config_helpers.py b/esphome/config_helpers.py
index 54242bc259..50ce4e8e34 100644
--- a/esphome/config_helpers.py
+++ b/esphome/config_helpers.py
@@ -1,4 +1,20 @@
-from esphome.const import CONF_ID
+from collections.abc import Callable
+
+from esphome.const import (
+    CONF_ID,
+    CONF_LEVEL,
+    CONF_LOGGER,
+    KEY_CORE,
+    KEY_TARGET_FRAMEWORK,
+    KEY_TARGET_PLATFORM,
+    PlatformFramework,
+)
+from esphome.core import CORE
+
+# Pre-build lookup map from (platform, framework) tuples to PlatformFramework enum
+_PLATFORM_FRAMEWORK_LOOKUP = {
+    (pf.value[0].value, pf.value[1].value): pf for pf in PlatformFramework
+}
 
 
 class Extend:
@@ -103,3 +119,60 @@ def merge_config(full_old, full_new):
         return new
 
     return merge(full_old, full_new)
+
+
+def filter_source_files_from_platform(
+    files_map: dict[str, set[PlatformFramework]],
+) -> Callable[[], list[str]]:
+    """Helper to build a FILTER_SOURCE_FILES function from platform mapping.
+
+    Args:
+        files_map: Dict mapping filename to set of PlatformFramework enums
+                  that should compile this file
+
+    Returns:
+        Function that returns list of files to exclude for current platform
+    """
+
+    def filter_source_files() -> list[str]:
+        # Get current platform/framework
+        core_data = CORE.data.get(KEY_CORE, {})
+        target_platform = core_data.get(KEY_TARGET_PLATFORM)
+        target_framework = core_data.get(KEY_TARGET_FRAMEWORK)
+
+        if not target_platform or not target_framework:
+            return []
+
+        # Direct lookup of current PlatformFramework
+        current_platform_framework = _PLATFORM_FRAMEWORK_LOOKUP.get(
+            (target_platform, target_framework)
+        )
+
+        if not current_platform_framework:
+            return []
+
+        # Return files that should be excluded for current platform
+        return [
+            filename
+            for filename, platforms in files_map.items()
+            if current_platform_framework not in platforms
+        ]
+
+    return filter_source_files
+
+
+def get_logger_level() -> str:
+    """Get the configured logger level.
+
+    This is used by components to determine what logging features to include
+    based on the configured log level.
+
+    Returns:
+        The configured logger level string, defaults to "DEBUG" if not configured
+    """
+    # Check if logger config exists
+    if CONF_LOGGER not in CORE.config:
+        return "DEBUG"
+
+    logger_config = CORE.config[CONF_LOGGER]
+    return logger_config.get(CONF_LEVEL, "DEBUG")
diff --git a/esphome/config_validation.py b/esphome/config_validation.py
index bf69b81bb5..09b132a458 100644
--- a/esphome/config_validation.py
+++ b/esphome/config_validation.py
@@ -1,5 +1,7 @@
 """Helpers for config validation using voluptuous."""
 
+from __future__ import annotations
+
 from contextlib import contextmanager
 from dataclasses import dataclass
 from datetime import datetime
@@ -29,6 +31,7 @@ from esphome.const import (
     CONF_COMMAND_RETAIN,
     CONF_COMMAND_TOPIC,
     CONF_DAY,
+    CONF_DEVICE_ID,
     CONF_DISABLED_BY_DEFAULT,
     CONF_DISCOVERY,
     CONF_ENTITY_CATEGORY,
@@ -355,6 +358,13 @@ def icon(value):
     )
 
 
+def sub_device_id(value: str | None) -> core.ID:
+    # Lazy import to avoid circular imports
+    from esphome.core.config import Device
+
+    return use_id(Device)(value)
+
+
 def boolean(value):
     """Validate the given config option to be a boolean.
 
@@ -1896,6 +1906,7 @@ ENTITY_BASE_SCHEMA = Schema(
         Optional(CONF_DISABLED_BY_DEFAULT, default=False): boolean,
         Optional(CONF_ICON): icon,
         Optional(CONF_ENTITY_CATEGORY): entity_category,
+        Optional(CONF_DEVICE_ID): sub_device_id,
     }
 )
 
@@ -1964,7 +1975,7 @@ class Version:
         return f"{self.major}.{self.minor}.{self.patch}"
 
     @classmethod
-    def parse(cls, value: str) -> "Version":
+    def parse(cls, value: str) -> Version:
         match = re.match(r"^(\d+).(\d+).(\d+)-?\w*$", value)
         if match is None:
             raise ValueError(f"Not a valid version number {value}")
diff --git a/esphome/const.py b/esphome/const.py
index b3453eee3a..94d1379b37 100644
--- a/esphome/const.py
+++ b/esphome/const.py
@@ -1,19 +1,65 @@
 """Constants used by esphome."""
 
-__version__ = "2025.6.3"
+from enum import Enum
+
+from esphome.enum import StrEnum
+
+__version__ = "2025.7.0b1"
 
 ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
 VALID_SUBSTITUTIONS_CHARACTERS = (
     "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"
 )
 
-PLATFORM_BK72XX = "bk72xx"
-PLATFORM_ESP32 = "esp32"
-PLATFORM_ESP8266 = "esp8266"
-PLATFORM_HOST = "host"
-PLATFORM_LIBRETINY_OLDSTYLE = "libretiny"
-PLATFORM_RP2040 = "rp2040"
-PLATFORM_RTL87XX = "rtl87xx"
+
+class Platform(StrEnum):
+    """Platform identifiers for ESPHome."""
+
+    BK72XX = "bk72xx"
+    ESP32 = "esp32"
+    ESP8266 = "esp8266"
+    HOST = "host"
+    LIBRETINY_OLDSTYLE = "libretiny"
+    LN882X = "ln882x"
+    RP2040 = "rp2040"
+    RTL87XX = "rtl87xx"
+
+
+class Framework(StrEnum):
+    """Framework identifiers for ESPHome."""
+
+    ARDUINO = "arduino"
+    ESP_IDF = "esp-idf"
+    NATIVE = "host"
+
+
+class PlatformFramework(Enum):
+    """Combined platform-framework identifiers with tuple values."""
+
+    # ESP32 variants
+    ESP32_ARDUINO = (Platform.ESP32, Framework.ARDUINO)
+    ESP32_IDF = (Platform.ESP32, Framework.ESP_IDF)
+
+    # Arduino framework platforms
+    ESP8266_ARDUINO = (Platform.ESP8266, Framework.ARDUINO)
+    RP2040_ARDUINO = (Platform.RP2040, Framework.ARDUINO)
+    BK72XX_ARDUINO = (Platform.BK72XX, Framework.ARDUINO)
+    RTL87XX_ARDUINO = (Platform.RTL87XX, Framework.ARDUINO)
+    LN882X_ARDUINO = (Platform.LN882X, Framework.ARDUINO)
+
+    # Host platform (native)
+    HOST_NATIVE = (Platform.HOST, Framework.NATIVE)
+
+
+# Maintain backward compatibility by reassigning after enum definition
+PLATFORM_BK72XX = Platform.BK72XX
+PLATFORM_ESP32 = Platform.ESP32
+PLATFORM_ESP8266 = Platform.ESP8266
+PLATFORM_HOST = Platform.HOST
+PLATFORM_LIBRETINY_OLDSTYLE = Platform.LIBRETINY_OLDSTYLE
+PLATFORM_LN882X = Platform.LN882X
+PLATFORM_RP2040 = Platform.RP2040
+PLATFORM_RTL87XX = Platform.RTL87XX
 
 
 SOURCE_FILE_EXTENSIONS = {".cpp", ".hpp", ".h", ".c", ".tcc", ".ino"}
@@ -56,6 +102,8 @@ CONF_AP = "ap"
 CONF_APPARENT_POWER = "apparent_power"
 CONF_ARDUINO_VERSION = "arduino_version"
 CONF_AREA = "area"
+CONF_AREA_ID = "area_id"
+CONF_AREAS = "areas"
 CONF_ARGS = "args"
 CONF_ASSUMED_STATE = "assumed_state"
 CONF_AT = "at"
@@ -89,6 +137,7 @@ CONF_BIT_DEPTH = "bit_depth"
 CONF_BITS_PER_SAMPLE = "bits_per_sample"
 CONF_BLOCK = "block"
 CONF_BLUE = "blue"
+CONF_BLUETOOTH = "bluetooth"
 CONF_BOARD = "board"
 CONF_BOARD_FLASH_MODE = "board_flash_mode"
 CONF_BORDER = "border"
@@ -216,6 +265,8 @@ CONF_DEST = "dest"
 CONF_DEVICE = "device"
 CONF_DEVICE_CLASS = "device_class"
 CONF_DEVICE_FACTOR = "device_factor"
+CONF_DEVICE_ID = "device_id"
+CONF_DEVICES = "devices"
 CONF_DIELECTRIC_CONSTANT = "dielectric_constant"
 CONF_DIMENSIONS = "dimensions"
 CONF_DIO_PIN = "dio_pin"
@@ -527,7 +578,9 @@ CONF_MONTH = "month"
 CONF_MONTHS = "months"
 CONF_MOSI_PIN = "mosi_pin"
 CONF_MOTION = "motion"
+CONF_MOVE_THRESHOLD = "move_threshold"
 CONF_MOVEMENT_COUNTER = "movement_counter"
+CONF_MOVING_DISTANCE = "moving_distance"
 CONF_MQTT = "mqtt"
 CONF_MQTT_ID = "mqtt_id"
 CONF_MULTIPLE = "multiple"
@@ -646,6 +699,7 @@ CONF_PAYLOAD = "payload"
 CONF_PAYLOAD_AVAILABLE = "payload_available"
 CONF_PAYLOAD_NOT_AVAILABLE = "payload_not_available"
 CONF_PERIOD = "period"
+CONF_PERMITTIVITY = "permittivity"
 CONF_PH = "ph"
 CONF_PHASE_A = "phase_a"
 CONF_PHASE_ANGLE = "phase_angle"
@@ -835,6 +889,7 @@ CONF_STEP = "step"
 CONF_STEP_DELAY = "step_delay"
 CONF_STEP_MODE = "step_mode"
 CONF_STEP_PIN = "step_pin"
+CONF_STILL_THRESHOLD = "still_threshold"
 CONF_STOP = "stop"
 CONF_STOP_ACTION = "stop_action"
 CONF_STORE_BASELINE = "store_baseline"
@@ -1091,7 +1146,7 @@ UNIT_KILOMETER_PER_HOUR = "km/h"
 UNIT_KILOVOLT_AMPS = "kVA"
 UNIT_KILOVOLT_AMPS_HOURS = "kVAh"
 UNIT_KILOVOLT_AMPS_REACTIVE = "kVAR"
-UNIT_KILOVOLT_AMPS_REACTIVE_HOURS = "kVARh"
+UNIT_KILOVOLT_AMPS_REACTIVE_HOURS = "kvarh"
 UNIT_KILOWATT = "kW"
 UNIT_KILOWATT_HOURS = "kWh"
 UNIT_LITRE = "L"
@@ -1127,7 +1182,7 @@ UNIT_VOLT = "V"
 UNIT_VOLT_AMPS = "VA"
 UNIT_VOLT_AMPS_HOURS = "VAh"
 UNIT_VOLT_AMPS_REACTIVE = "var"
-UNIT_VOLT_AMPS_REACTIVE_HOURS = "VARh"
+UNIT_VOLT_AMPS_REACTIVE_HOURS = "varh"
 UNIT_WATT = "W"
 UNIT_WATT_HOURS = "Wh"
 
diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py
index e95bd7edcc..e33bbcf726 100644
--- a/esphome/core/__init__.py
+++ b/esphome/core/__init__.py
@@ -20,6 +20,7 @@ from esphome.const import (
     PLATFORM_ESP32,
     PLATFORM_ESP8266,
     PLATFORM_HOST,
+    PLATFORM_LN882X,
     PLATFORM_RP2040,
     PLATFORM_RTL87XX,
 )
@@ -507,6 +508,8 @@ class EsphomeCore:
         self.libraries: list[Library] = []
         # A set of build flags to set in the platformio project
         self.build_flags: set[str] = set()
+        # A set of build unflags to set in the platformio project
+        self.build_unflags: set[str] = set()
         # A set of defines to set for the compile process in esphome/core/defines.h
         self.defines: set[Define] = set()
         # A map of all platformio options to apply
@@ -520,6 +523,9 @@ class EsphomeCore:
         # Dict to track platform entity counts for pre-allocation
         # Key: platform name (e.g. "sensor", "binary_sensor"), Value: count
         self.platform_counts: defaultdict[str, int] = defaultdict(int)
+        # Track entity unique IDs to handle duplicates
+        # Set of (device_id, platform, sanitized_name) tuples
+        self.unique_ids: set[tuple[str, str, str]] = set()
         # Whether ESPHome was started in verbose mode
         self.verbose = False
         # Whether ESPHome was started in quiet mode
@@ -545,11 +551,13 @@ class EsphomeCore:
         self.global_statements = []
         self.libraries = []
         self.build_flags = set()
+        self.build_unflags = set()
         self.defines = set()
         self.platformio_options = {}
         self.loaded_integrations = set()
         self.component_ids = set()
         self.platform_counts = defaultdict(int)
+        self.unique_ids = set()
         PIN_SCHEMA_REGISTRY.reset()
 
     @property
@@ -654,9 +662,13 @@ class EsphomeCore:
     def is_rtl87xx(self):
         return self.target_platform == PLATFORM_RTL87XX
 
+    @property
+    def is_ln882x(self):
+        return self.target_platform == PLATFORM_LN882X
+
     @property
     def is_libretiny(self):
-        return self.is_bk72xx or self.is_rtl87xx
+        return self.is_bk72xx or self.is_rtl87xx or self.is_ln882x
 
     @property
     def is_host(self):
@@ -766,11 +778,15 @@ class EsphomeCore:
             self.libraries.append(library)
         return library
 
-    def add_build_flag(self, build_flag):
+    def add_build_flag(self, build_flag: str) -> str:
         self.build_flags.add(build_flag)
         _LOGGER.debug("Adding build flag: %s", build_flag)
         return build_flag
 
+    def add_build_unflag(self, build_unflag: str) -> None:
+        self.build_unflags.add(build_unflag)
+        _LOGGER.debug("Adding build unflag: %s", build_unflag)
+
     def add_define(self, define):
         if isinstance(define, str):
             define = Define(define)
diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp
index 4ed96f7300..d6fab018cc 100644
--- a/esphome/core/application.cpp
+++ b/esphome/core/application.cpp
@@ -3,6 +3,7 @@
 #include "esphome/core/version.h"
 #include "esphome/core/hal.h"
 #include 
+#include 
 
 #ifdef USE_STATUS_LED
 #include "esphome/components/status_led/status_led.h"
@@ -83,6 +84,10 @@ void Application::setup() {
   }
 
   ESP_LOGI(TAG, "setup() finished successfully!");
+
+  // Clear setup priority overrides to free memory
+  clear_setup_priority_overrides();
+
   this->schedule_dump_config();
   this->calculate_looping_components_();
 }
@@ -97,7 +102,27 @@ void Application::loop() {
   // Feed WDT with time
   this->feed_wdt(last_op_end_time);
 
-  for (Component *component : this->looping_components_) {
+  // Process any pending enable_loop requests from ISRs
+  // This must be done before marking in_loop_ = true to avoid race conditions
+  if (this->has_pending_enable_loop_requests_) {
+    // Clear flag BEFORE processing to avoid race condition
+    // If ISR sets it during processing, we'll catch it next loop iteration
+    // This is safe because:
+    // 1. Each component has its own pending_enable_loop_ flag that we check
+    // 2. If we can't process a component (wrong state), enable_pending_loops_()
+    //    will set this flag back to true
+    // 3. Any new ISR requests during processing will set the flag again
+    this->has_pending_enable_loop_requests_ = false;
+    this->enable_pending_loops_();
+  }
+
+  // Mark that we're in the loop for safe reentrant modifications
+  this->in_loop_ = true;
+
+  for (this->current_loop_index_ = 0; this->current_loop_index_ < this->looping_components_active_end_;
+       this->current_loop_index_++) {
+    Component *component = this->looping_components_[this->current_loop_index_];
+
     // Update the cached time before each component runs
     this->loop_component_start_time_ = last_op_end_time;
 
@@ -112,6 +137,8 @@ void Application::loop() {
     this->app_state_ |= new_app_state;
     this->feed_wdt(last_op_end_time);
   }
+
+  this->in_loop_ = false;
   this->app_state_ = new_app_state;
 
   // Use the last component's end time instead of calling millis() again
@@ -162,8 +189,8 @@ void IRAM_ATTR HOT Application::feed_wdt(uint32_t time) {
 }
 void Application::reboot() {
   ESP_LOGI(TAG, "Forcing a reboot");
-  for (auto it = this->components_.rbegin(); it != this->components_.rend(); ++it) {
-    (*it)->on_shutdown();
+  for (auto &component : std::ranges::reverse_view(this->components_)) {
+    component->on_shutdown();
   }
   arch_restart();
 }
@@ -176,17 +203,17 @@ void Application::safe_reboot() {
 }
 
 void Application::run_safe_shutdown_hooks() {
-  for (auto it = this->components_.rbegin(); it != this->components_.rend(); ++it) {
-    (*it)->on_safe_shutdown();
+  for (auto &component : std::ranges::reverse_view(this->components_)) {
+    component->on_safe_shutdown();
   }
-  for (auto it = this->components_.rbegin(); it != this->components_.rend(); ++it) {
-    (*it)->on_shutdown();
+  for (auto &component : std::ranges::reverse_view(this->components_)) {
+    component->on_shutdown();
   }
 }
 
 void Application::run_powerdown_hooks() {
-  for (auto it = this->components_.rbegin(); it != this->components_.rend(); ++it) {
-    (*it)->on_powerdown();
+  for (auto &component : std::ranges::reverse_view(this->components_)) {
+    component->on_powerdown();
   }
 }
 
@@ -235,9 +262,135 @@ void Application::teardown_components(uint32_t timeout_ms) {
 }
 
 void Application::calculate_looping_components_() {
+  // Count total components that need looping
+  size_t total_looping = 0;
   for (auto *obj : this->components_) {
-    if (obj->has_overridden_loop())
+    if (obj->has_overridden_loop()) {
+      total_looping++;
+    }
+  }
+
+  // Pre-reserve vector to avoid reallocations
+  this->looping_components_.reserve(total_looping);
+
+  // First add all active components
+  for (auto *obj : this->components_) {
+    if (obj->has_overridden_loop() &&
+        (obj->get_component_state() & COMPONENT_STATE_MASK) != COMPONENT_STATE_LOOP_DONE) {
       this->looping_components_.push_back(obj);
+    }
+  }
+
+  this->looping_components_active_end_ = this->looping_components_.size();
+
+  // Then add all inactive (LOOP_DONE) components
+  // This handles components that called disable_loop() during setup, before this method runs
+  for (auto *obj : this->components_) {
+    if (obj->has_overridden_loop() &&
+        (obj->get_component_state() & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE) {
+      this->looping_components_.push_back(obj);
+    }
+  }
+}
+
+void Application::disable_component_loop_(Component *component) {
+  // This method must be reentrant - components can disable themselves during their own loop() call
+  // Linear search to find component in active section
+  // Most configs have 10-30 looping components (30 is on the high end)
+  // O(n) is acceptable here as we optimize for memory, not complexity
+  for (uint16_t i = 0; i < this->looping_components_active_end_; i++) {
+    if (this->looping_components_[i] == component) {
+      // Move last active component to this position
+      this->looping_components_active_end_--;
+      if (i != this->looping_components_active_end_) {
+        std::swap(this->looping_components_[i], this->looping_components_[this->looping_components_active_end_]);
+
+        // If we're currently iterating and just swapped the current position
+        if (this->in_loop_ && i == this->current_loop_index_) {
+          // Decrement so we'll process the swapped component next
+          this->current_loop_index_--;
+        }
+      }
+      return;
+    }
+  }
+}
+
+void Application::activate_looping_component_(uint16_t index) {
+  // Helper to move component from inactive to active section
+  if (index != this->looping_components_active_end_) {
+    std::swap(this->looping_components_[index], this->looping_components_[this->looping_components_active_end_]);
+  }
+  this->looping_components_active_end_++;
+}
+
+void Application::enable_component_loop_(Component *component) {
+  // This method is only called when component state is LOOP_DONE, so we know
+  // the component must be in the inactive section (if it exists in looping_components_)
+  // Only search the inactive portion for better performance
+  // With typical 0-5 inactive components, O(k) is much faster than O(n)
+  const uint16_t size = this->looping_components_.size();
+  for (uint16_t i = this->looping_components_active_end_; i < size; i++) {
+    if (this->looping_components_[i] == component) {
+      // Found in inactive section - move to active
+      this->activate_looping_component_(i);
+      return;
+    }
+  }
+  // Component not found in looping_components_ - this is normal for components
+  // that don't have loop() or were not included in the partitioned vector
+}
+
+void Application::enable_pending_loops_() {
+  // Process components that requested enable_loop from ISR context
+  // Only iterate through inactive looping_components_ (typically 0-5) instead of all components
+  //
+  // Race condition handling:
+  // 1. We check if component is already in LOOP state first - if so, just clear the flag
+  //    This handles reentrancy where enable_loop() was called between ISR and processing
+  // 2. We only clear pending_enable_loop_ after checking state, preventing lost requests
+  // 3. If any components aren't in LOOP_DONE state, we set has_pending_enable_loop_requests_
+  //    back to true to ensure we check again next iteration
+  // 4. ISRs can safely set flags at any time - worst case is we process them next iteration
+  // 5. The global flag (has_pending_enable_loop_requests_) is cleared before this method,
+  //    so any ISR that fires during processing will be caught in the next loop
+  const uint16_t size = this->looping_components_.size();
+  bool has_pending = false;
+
+  for (uint16_t i = this->looping_components_active_end_; i < size; i++) {
+    Component *component = this->looping_components_[i];
+    if (!component->pending_enable_loop_) {
+      continue;  // Skip components without pending requests
+    }
+
+    // Check current state
+    uint8_t state = component->component_state_ & COMPONENT_STATE_MASK;
+
+    // If already in LOOP state, nothing to do - clear flag and continue
+    if (state == COMPONENT_STATE_LOOP) {
+      component->pending_enable_loop_ = false;
+      continue;
+    }
+
+    // If not in LOOP_DONE state, can't enable yet - keep flag set
+    if (state != COMPONENT_STATE_LOOP_DONE) {
+      has_pending = true;  // Keep tracking this component
+      continue;            // Keep the flag set - try again next iteration
+    }
+
+    // Clear the pending flag and enable the loop
+    component->pending_enable_loop_ = false;
+    ESP_LOGVV(TAG, "%s loop enabled from ISR", component->get_component_source());
+    component->component_state_ &= ~COMPONENT_STATE_MASK;
+    component->component_state_ |= COMPONENT_STATE_LOOP;
+
+    // Move to active section
+    this->activate_looping_component_(i);
+  }
+
+  // If we couldn't process some requests, ensure we check again next iteration
+  if (has_pending) {
+    this->has_pending_enable_loop_requests_ = true;
   }
 }
 
diff --git a/esphome/core/application.h b/esphome/core/application.h
index f04ea05d8e..f2b5cb5c89 100644
--- a/esphome/core/application.h
+++ b/esphome/core/application.h
@@ -1,5 +1,7 @@
 #pragma once
 
+#include 
+#include 
 #include 
 #include 
 #include "esphome/core/component.h"
@@ -9,6 +11,13 @@
 #include "esphome/core/preferences.h"
 #include "esphome/core/scheduler.h"
 
+#ifdef USE_DEVICES
+#include "esphome/core/device.h"
+#endif
+#ifdef USE_AREAS
+#include "esphome/core/area.h"
+#endif
+
 #ifdef USE_SOCKET_SELECT_SUPPORT
 #include 
 #endif
@@ -87,7 +96,7 @@ static const uint32_t TEARDOWN_TIMEOUT_REBOOT_MS = 1000;  // 1 second for quick
 
 class Application {
  public:
-  void pre_setup(const std::string &name, const std::string &friendly_name, const char *area, const char *comment,
+  void pre_setup(const std::string &name, const std::string &friendly_name, const char *comment,
                  const char *compilation_time, bool name_add_mac_suffix) {
     arch_init();
     this->name_add_mac_suffix_ = name_add_mac_suffix;
@@ -102,11 +111,17 @@ class Application {
       this->name_ = name;
       this->friendly_name_ = friendly_name;
     }
-    this->area_ = area;
     this->comment_ = comment;
     this->compilation_time_ = compilation_time;
   }
 
+#ifdef USE_DEVICES
+  void register_device(Device *device) { this->devices_.push_back(device); }
+#endif
+#ifdef USE_AREAS
+  void register_area(Area *area) { this->areas_.push_back(area); }
+#endif
+
   void set_current_component(Component *component) { this->current_component_ = component; }
   Component *get_current_component() { return this->current_component_; }
 
@@ -264,6 +279,12 @@ class Application {
 #ifdef USE_UPDATE
   void reserve_update(size_t count) { this->updates_.reserve(count); }
 #endif
+#ifdef USE_AREAS
+  void reserve_area(size_t count) { this->areas_.reserve(count); }
+#endif
+#ifdef USE_DEVICES
+  void reserve_device(size_t count) { this->devices_.reserve(count); }
+#endif
 
   /// Register the component in this Application instance.
   template C *register_component(C *c) {
@@ -285,7 +306,15 @@ class Application {
   const std::string &get_friendly_name() const { return this->friendly_name_; }
 
   /// Get the area of this Application set by pre_setup().
-  std::string get_area() const { return this->area_ == nullptr ? "" : this->area_; }
+  const char *get_area() const {
+#ifdef USE_AREAS
+    // If we have areas registered, return the name of the first one (which is the top-level area)
+    if (!this->areas_.empty() && this->areas_[0] != nullptr) {
+      return this->areas_[0]->get_name();
+    }
+#endif
+    return "";
+  }
 
   /// Get the comment of this Application set by pre_setup().
   std::string get_comment() const { return this->comment_; }
@@ -308,11 +337,16 @@ class Application {
    * Each component can request a high frequency loop execution by using the HighFrequencyLoopRequester
    * helper in helpers.h
    *
+   * Note: This method is not called by ESPHome core code. It is only used by lambda functions
+   * in YAML configurations or by external components.
+   *
    * @param loop_interval The interval in milliseconds to run the core loop at. Defaults to 16 milliseconds.
    */
-  void set_loop_interval(uint32_t loop_interval) { this->loop_interval_ = loop_interval; }
+  void set_loop_interval(uint32_t loop_interval) {
+    this->loop_interval_ = std::min(loop_interval, static_cast(std::numeric_limits::max()));
+  }
 
-  uint32_t get_loop_interval() const { return this->loop_interval_; }
+  uint32_t get_loop_interval() const { return static_cast(this->loop_interval_); }
 
   void schedule_dump_config() { this->dump_config_at_ = 0; }
 
@@ -334,220 +368,111 @@ class Application {
 
   uint8_t get_app_state() const { return this->app_state_; }
 
+// Helper macro for entity getter method declarations - reduces code duplication
+// When USE_DEVICE_ID is enabled in the future, this can be conditionally compiled to add device_id parameter
+#define GET_ENTITY_METHOD(entity_type, entity_name, entities_member) \
+  entity_type *get_##entity_name##_by_key(uint32_t key, bool include_internal = false) { \
+    for (auto *obj : this->entities_member##_) { \
+      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) \
+        return obj; \
+    } \
+    return nullptr; \
+  }
+
+#ifdef USE_DEVICES
+  const std::vector &get_devices() { return this->devices_; }
+#endif
+#ifdef USE_AREAS
+  const std::vector &get_areas() { return this->areas_; }
+#endif
 #ifdef USE_BINARY_SENSOR
   const std::vector &get_binary_sensors() { return this->binary_sensors_; }
-  binary_sensor::BinarySensor *get_binary_sensor_by_key(uint32_t key, bool include_internal = false) {
-    for (auto *obj : this->binary_sensors_) {
-      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
-        return obj;
-    }
-    return nullptr;
-  }
+  GET_ENTITY_METHOD(binary_sensor::BinarySensor, binary_sensor, binary_sensors)
 #endif
 #ifdef USE_SWITCH
   const std::vector &get_switches() { return this->switches_; }
-  switch_::Switch *get_switch_by_key(uint32_t key, bool include_internal = false) {
-    for (auto *obj : this->switches_) {
-      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
-        return obj;
-    }
-    return nullptr;
-  }
+  GET_ENTITY_METHOD(switch_::Switch, switch, switches)
 #endif
 #ifdef USE_BUTTON
   const std::vector &get_buttons() { return this->buttons_; }
-  button::Button *get_button_by_key(uint32_t key, bool include_internal = false) {
-    for (auto *obj : this->buttons_) {
-      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
-        return obj;
-    }
-    return nullptr;
-  }
+  GET_ENTITY_METHOD(button::Button, button, buttons)
 #endif
 #ifdef USE_SENSOR
   const std::vector &get_sensors() { return this->sensors_; }
-  sensor::Sensor *get_sensor_by_key(uint32_t key, bool include_internal = false) {
-    for (auto *obj : this->sensors_) {
-      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
-        return obj;
-    }
-    return nullptr;
-  }
+  GET_ENTITY_METHOD(sensor::Sensor, sensor, sensors)
 #endif
 #ifdef USE_TEXT_SENSOR
   const std::vector &get_text_sensors() { return this->text_sensors_; }
-  text_sensor::TextSensor *get_text_sensor_by_key(uint32_t key, bool include_internal = false) {
-    for (auto *obj : this->text_sensors_) {
-      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
-        return obj;
-    }
-    return nullptr;
-  }
+  GET_ENTITY_METHOD(text_sensor::TextSensor, text_sensor, text_sensors)
 #endif
 #ifdef USE_FAN
   const std::vector &get_fans() { return this->fans_; }
-  fan::Fan *get_fan_by_key(uint32_t key, bool include_internal = false) {
-    for (auto *obj : this->fans_) {
-      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
-        return obj;
-    }
-    return nullptr;
-  }
+  GET_ENTITY_METHOD(fan::Fan, fan, fans)
 #endif
 #ifdef USE_COVER
   const std::vector &get_covers() { return this->covers_; }
-  cover::Cover *get_cover_by_key(uint32_t key, bool include_internal = false) {
-    for (auto *obj : this->covers_) {
-      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
-        return obj;
-    }
-    return nullptr;
-  }
+  GET_ENTITY_METHOD(cover::Cover, cover, covers)
 #endif
 #ifdef USE_LIGHT
   const std::vector &get_lights() { return this->lights_; }
-  light::LightState *get_light_by_key(uint32_t key, bool include_internal = false) {
-    for (auto *obj : this->lights_) {
-      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
-        return obj;
-    }
-    return nullptr;
-  }
+  GET_ENTITY_METHOD(light::LightState, light, lights)
 #endif
 #ifdef USE_CLIMATE
   const std::vector &get_climates() { return this->climates_; }
-  climate::Climate *get_climate_by_key(uint32_t key, bool include_internal = false) {
-    for (auto *obj : this->climates_) {
-      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
-        return obj;
-    }
-    return nullptr;
-  }
+  GET_ENTITY_METHOD(climate::Climate, climate, climates)
 #endif
 #ifdef USE_NUMBER
   const std::vector &get_numbers() { return this->numbers_; }
-  number::Number *get_number_by_key(uint32_t key, bool include_internal = false) {
-    for (auto *obj : this->numbers_) {
-      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
-        return obj;
-    }
-    return nullptr;
-  }
+  GET_ENTITY_METHOD(number::Number, number, numbers)
 #endif
 #ifdef USE_DATETIME_DATE
   const std::vector &get_dates() { return this->dates_; }
-  datetime::DateEntity *get_date_by_key(uint32_t key, bool include_internal = false) {
-    for (auto *obj : this->dates_) {
-      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
-        return obj;
-    }
-    return nullptr;
-  }
+  GET_ENTITY_METHOD(datetime::DateEntity, date, dates)
 #endif
 #ifdef USE_DATETIME_TIME
   const std::vector &get_times() { return this->times_; }
-  datetime::TimeEntity *get_time_by_key(uint32_t key, bool include_internal = false) {
-    for (auto *obj : this->times_) {
-      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
-        return obj;
-    }
-    return nullptr;
-  }
+  GET_ENTITY_METHOD(datetime::TimeEntity, time, times)
 #endif
 #ifdef USE_DATETIME_DATETIME
   const std::vector &get_datetimes() { return this->datetimes_; }
-  datetime::DateTimeEntity *get_datetime_by_key(uint32_t key, bool include_internal = false) {
-    for (auto *obj : this->datetimes_) {
-      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
-        return obj;
-    }
-    return nullptr;
-  }
+  GET_ENTITY_METHOD(datetime::DateTimeEntity, datetime, datetimes)
 #endif
 #ifdef USE_TEXT
   const std::vector &get_texts() { return this->texts_; }
-  text::Text *get_text_by_key(uint32_t key, bool include_internal = false) {
-    for (auto *obj : this->texts_) {
-      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
-        return obj;
-    }
-    return nullptr;
-  }
+  GET_ENTITY_METHOD(text::Text, text, texts)
 #endif
 #ifdef USE_SELECT
   const std::vector &get_selects() { return this->selects_; }
-  select::Select *get_select_by_key(uint32_t key, bool include_internal = false) {
-    for (auto *obj : this->selects_) {
-      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
-        return obj;
-    }
-    return nullptr;
-  }
+  GET_ENTITY_METHOD(select::Select, select, selects)
 #endif
 #ifdef USE_LOCK
   const std::vector &get_locks() { return this->locks_; }
-  lock::Lock *get_lock_by_key(uint32_t key, bool include_internal = false) {
-    for (auto *obj : this->locks_) {
-      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
-        return obj;
-    }
-    return nullptr;
-  }
+  GET_ENTITY_METHOD(lock::Lock, lock, locks)
 #endif
 #ifdef USE_VALVE
   const std::vector &get_valves() { return this->valves_; }
-  valve::Valve *get_valve_by_key(uint32_t key, bool include_internal = false) {
-    for (auto *obj : this->valves_) {
-      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
-        return obj;
-    }
-    return nullptr;
-  }
+  GET_ENTITY_METHOD(valve::Valve, valve, valves)
 #endif
 #ifdef USE_MEDIA_PLAYER
   const std::vector &get_media_players() { return this->media_players_; }
-  media_player::MediaPlayer *get_media_player_by_key(uint32_t key, bool include_internal = false) {
-    for (auto *obj : this->media_players_) {
-      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
-        return obj;
-    }
-    return nullptr;
-  }
+  GET_ENTITY_METHOD(media_player::MediaPlayer, media_player, media_players)
 #endif
 
 #ifdef USE_ALARM_CONTROL_PANEL
   const std::vector &get_alarm_control_panels() {
     return this->alarm_control_panels_;
   }
-  alarm_control_panel::AlarmControlPanel *get_alarm_control_panel_by_key(uint32_t key, bool include_internal = false) {
-    for (auto *obj : this->alarm_control_panels_) {
-      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
-        return obj;
-    }
-    return nullptr;
-  }
+  GET_ENTITY_METHOD(alarm_control_panel::AlarmControlPanel, alarm_control_panel, alarm_control_panels)
 #endif
 
 #ifdef USE_EVENT
   const std::vector &get_events() { return this->events_; }
-  event::Event *get_event_by_key(uint32_t key, bool include_internal = false) {
-    for (auto *obj : this->events_) {
-      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
-        return obj;
-    }
-    return nullptr;
-  }
+  GET_ENTITY_METHOD(event::Event, event, events)
 #endif
 
 #ifdef USE_UPDATE
   const std::vector &get_updates() { return this->updates_; }
-  update::UpdateEntity *get_update_by_key(uint32_t key, bool include_internal = false) {
-    for (auto *obj : this->updates_) {
-      if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal()))
-        return obj;
-    }
-    return nullptr;
-  }
+  GET_ENTITY_METHOD(update::UpdateEntity, update, updates)
 #endif
 
   Scheduler scheduler;
@@ -572,14 +497,56 @@ class Application {
 
   void calculate_looping_components_();
 
+  // These methods are called by Component::disable_loop() and Component::enable_loop()
+  // Components should not call these directly - use this->disable_loop() or this->enable_loop()
+  // to ensure component state is properly updated along with the loop partition
+  void disable_component_loop_(Component *component);
+  void enable_component_loop_(Component *component);
+  void enable_pending_loops_();
+  void activate_looping_component_(uint16_t index);
+
   void feed_wdt_arch_();
 
   /// Perform a delay while also monitoring socket file descriptors for readiness
   void yield_with_select_(uint32_t delay_ms);
 
+  // === Member variables ordered by size to minimize padding ===
+
+  // Pointer-sized members first
+  Component *current_component_{nullptr};
+  const char *comment_{nullptr};
+  const char *compilation_time_{nullptr};
+
+  // size_t members
+  size_t dump_config_at_{SIZE_MAX};
+
+  // Vectors (largest members)
   std::vector components_{};
+
+  // Partitioned vector design for looping components
+  // =================================================
+  // Components are partitioned into [active | inactive] sections:
+  //
+  // looping_components_: [A, B, C, D | E, F]
+  //                                  ^
+  //                      looping_components_active_end_ (4)
+  //
+  // - Components A,B,C,D are active and will be called in loop()
+  // - Components E,F are inactive (disabled/failed) and won't be called
+  // - No flag checking needed during iteration - just loop 0 to active_end_
+  // - When a component is disabled, it's swapped with the last active component
+  //   and active_end_ is decremented
+  // - When a component is enabled, it's swapped with the first inactive component
+  //   and active_end_ is incremented
+  // - This eliminates branch mispredictions from flag checking in the hot loop
   std::vector looping_components_{};
 
+#ifdef USE_DEVICES
+  std::vector devices_{};
+#endif
+#ifdef USE_AREAS
+  std::vector areas_{};
+#endif
 #ifdef USE_BINARY_SENSOR
   std::vector binary_sensors_{};
 #endif
@@ -644,26 +611,39 @@ class Application {
   std::vector updates_{};
 #endif
 
+#ifdef USE_SOCKET_SELECT_SUPPORT
+  std::vector socket_fds_;  // Vector of all monitored socket file descriptors
+#endif
+
+  // String members
   std::string name_;
   std::string friendly_name_;
-  const char *area_{nullptr};
-  const char *comment_{nullptr};
-  const char *compilation_time_{nullptr};
-  bool name_add_mac_suffix_;
+
+  // 4-byte members
   uint32_t last_loop_{0};
-  uint32_t loop_interval_{16};
-  size_t dump_config_at_{SIZE_MAX};
-  uint8_t app_state_{0};
-  Component *current_component_{nullptr};
   uint32_t loop_component_start_time_{0};
 
 #ifdef USE_SOCKET_SELECT_SUPPORT
-  // Socket select management
-  std::vector socket_fds_;     // Vector of all monitored socket file descriptors
+  int max_fd_{-1};  // Highest file descriptor number for select()
+#endif
+
+  // 2-byte members (grouped together for alignment)
+  uint16_t loop_interval_{16};  // Loop interval in ms (max 65535ms = 65.5 seconds)
+  uint16_t looping_components_active_end_{0};
+  uint16_t current_loop_index_{0};  // For safe reentrant modifications during iteration
+
+  // 1-byte members (grouped together to minimize padding)
+  uint8_t app_state_{0};
+  bool name_add_mac_suffix_;
+  bool in_loop_{false};
+  volatile bool has_pending_enable_loop_requests_{false};
+
+#ifdef USE_SOCKET_SELECT_SUPPORT
   bool socket_fds_changed_{false};  // Flag to rebuild base_read_fds_ when socket_fds_ changes
-  int max_fd_{-1};                  // Highest file descriptor number for select()
-  fd_set base_read_fds_{};          // Cached fd_set rebuilt only when socket_fds_ changes
-  fd_set read_fds_{};               // Working fd_set for select(), copied from base_read_fds_
+
+  // Variable-sized members at end
+  fd_set base_read_fds_{};  // Cached fd_set rebuilt only when socket_fds_ changes
+  fd_set read_fds_{};       // Working fd_set for select(), copied from base_read_fds_
 #endif
 };
 
diff --git a/esphome/core/area.h b/esphome/core/area.h
new file mode 100644
index 0000000000..f6d88fe703
--- /dev/null
+++ b/esphome/core/area.h
@@ -0,0 +1,19 @@
+#pragma once
+
+#include 
+
+namespace esphome {
+
+class Area {
+ public:
+  void set_area_id(uint32_t area_id) { this->area_id_ = area_id; }
+  uint32_t get_area_id() { return this->area_id_; }
+  void set_name(const char *name) { this->name_ = name; }
+  const char *get_name() { return this->name_; }
+
+ protected:
+  uint32_t area_id_{};
+  const char *name_ = "";
+};
+
+}  // namespace esphome
diff --git a/esphome/core/automation.h b/esphome/core/automation.h
index 02c9d44f16..e156818312 100644
--- a/esphome/core/automation.h
+++ b/esphome/core/automation.h
@@ -27,20 +27,67 @@ template class TemplatableValue {
  public:
   TemplatableValue() : type_(NONE) {}
 
-  template::value, int> = 0>
-  TemplatableValue(F value) : type_(VALUE), value_(std::move(value)) {}
+  template::value, int> = 0> TemplatableValue(F value) : type_(VALUE) {
+    new (&this->value_) T(std::move(value));
+  }
 
-  template::value, int> = 0>
-  TemplatableValue(F f) : type_(LAMBDA), f_(f) {}
+  template::value, int> = 0> TemplatableValue(F f) : type_(LAMBDA) {
+    this->f_ = new std::function(std::move(f));
+  }
+
+  // Copy constructor
+  TemplatableValue(const TemplatableValue &other) : type_(other.type_) {
+    if (type_ == VALUE) {
+      new (&this->value_) T(other.value_);
+    } else if (type_ == LAMBDA) {
+      this->f_ = new std::function(*other.f_);
+    }
+  }
+
+  // Move constructor
+  TemplatableValue(TemplatableValue &&other) noexcept : type_(other.type_) {
+    if (type_ == VALUE) {
+      new (&this->value_) T(std::move(other.value_));
+    } else if (type_ == LAMBDA) {
+      this->f_ = other.f_;
+      other.f_ = nullptr;
+    }
+    other.type_ = NONE;
+  }
+
+  // Assignment operators
+  TemplatableValue &operator=(const TemplatableValue &other) {
+    if (this != &other) {
+      this->~TemplatableValue();
+      new (this) TemplatableValue(other);
+    }
+    return *this;
+  }
+
+  TemplatableValue &operator=(TemplatableValue &&other) noexcept {
+    if (this != &other) {
+      this->~TemplatableValue();
+      new (this) TemplatableValue(std::move(other));
+    }
+    return *this;
+  }
+
+  ~TemplatableValue() {
+    if (type_ == VALUE) {
+      this->value_.~T();
+    } else if (type_ == LAMBDA) {
+      delete this->f_;
+    }
+  }
 
   bool has_value() { return this->type_ != NONE; }
 
   T value(X... x) {
     if (this->type_ == LAMBDA) {
-      return this->f_(x...);
+      return (*this->f_)(x...);
     }
     // return value also when none
-    return this->value_;
+    return this->type_ == VALUE ? this->value_ : T{};
   }
 
   optional optional_value(X... x) {
@@ -58,14 +105,16 @@ template class TemplatableValue {
   }
 
  protected:
-  enum {
+  enum : uint8_t {
     NONE,
     VALUE,
     LAMBDA,
   } type_;
 
-  T value_{};
-  std::function f_{};
+  union {
+    T value_;
+    std::function *f_;
+  };
 };
 
 /** Base class for all automation conditions.
diff --git a/esphome/core/color.cpp b/esphome/core/color.cpp
index 58d995db2f..7e390b2354 100644
--- a/esphome/core/color.cpp
+++ b/esphome/core/color.cpp
@@ -2,10 +2,8 @@
 
 namespace esphome {
 
-const Color Color::BLACK(0, 0, 0, 0);
-const Color Color::WHITE(255, 255, 255, 255);
-
-const Color COLOR_BLACK(0, 0, 0, 0);
-const Color COLOR_WHITE(255, 255, 255, 255);
+// C++20 constinit ensures compile-time initialization (stored in ROM)
+constinit const Color Color::BLACK(0, 0, 0, 0);
+constinit const Color Color::WHITE(255, 255, 255, 255);
 
 }  // namespace esphome
diff --git a/esphome/core/color.h b/esphome/core/color.h
index 1c43fd9d3e..2b307bb438 100644
--- a/esphome/core/color.h
+++ b/esphome/core/color.h
@@ -5,7 +5,9 @@
 
 namespace esphome {
 
-inline static uint8_t esp_scale8(uint8_t i, uint8_t scale) { return (uint16_t(i) * (1 + uint16_t(scale))) / 256; }
+inline static constexpr uint8_t esp_scale8(uint8_t i, uint8_t scale) {
+  return (uint16_t(i) * (1 + uint16_t(scale))) / 256;
+}
 
 struct Color {
   union {
@@ -31,17 +33,20 @@ struct Color {
     uint32_t raw_32;
   };
 
-  inline Color() ESPHOME_ALWAYS_INLINE : r(0), g(0), b(0), w(0) {}  // NOLINT
-  inline Color(uint8_t red, uint8_t green, uint8_t blue) ESPHOME_ALWAYS_INLINE : r(red), g(green), b(blue), w(0) {}
+  inline constexpr Color() ESPHOME_ALWAYS_INLINE : raw_32(0) {}  // NOLINT
+  inline constexpr Color(uint8_t red, uint8_t green, uint8_t blue) ESPHOME_ALWAYS_INLINE : r(red),
+                                                                                           g(green),
+                                                                                           b(blue),
+                                                                                           w(0) {}
 
-  inline Color(uint8_t red, uint8_t green, uint8_t blue, uint8_t white) ESPHOME_ALWAYS_INLINE : r(red),
-                                                                                                g(green),
-                                                                                                b(blue),
-                                                                                                w(white) {}
-  inline explicit Color(uint32_t colorcode) ESPHOME_ALWAYS_INLINE : r((colorcode >> 16) & 0xFF),
-                                                                    g((colorcode >> 8) & 0xFF),
-                                                                    b((colorcode >> 0) & 0xFF),
-                                                                    w((colorcode >> 24) & 0xFF) {}
+  inline constexpr Color(uint8_t red, uint8_t green, uint8_t blue, uint8_t white) ESPHOME_ALWAYS_INLINE : r(red),
+                                                                                                          g(green),
+                                                                                                          b(blue),
+                                                                                                          w(white) {}
+  inline explicit constexpr Color(uint32_t colorcode) ESPHOME_ALWAYS_INLINE : r((colorcode >> 16) & 0xFF),
+                                                                              g((colorcode >> 8) & 0xFF),
+                                                                              b((colorcode >> 0) & 0xFF),
+                                                                              w((colorcode >> 24) & 0xFF) {}
 
   inline bool is_on() ESPHOME_ALWAYS_INLINE { return this->raw_32 != 0; }
 
@@ -169,9 +174,4 @@ struct Color {
   static const Color WHITE;
 };
 
-ESPDEPRECATED("Use Color::BLACK instead of COLOR_BLACK", "v1.21")
-extern const Color COLOR_BLACK;
-ESPDEPRECATED("Use Color::WHITE instead of COLOR_WHITE", "v1.21")
-extern const Color COLOR_WHITE;
-
 }  // namespace esphome
diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp
index 03c44599e2..9d863e56cd 100644
--- a/esphome/core/component.cpp
+++ b/esphome/core/component.cpp
@@ -2,7 +2,9 @@
 
 #include 
 #include 
+#include 
 #include 
+#include 
 #include "esphome/core/application.h"
 #include "esphome/core/hal.h"
 #include "esphome/core/helpers.h"
@@ -12,6 +14,30 @@ namespace esphome {
 
 static const char *const TAG = "component";
 
+// Global vectors for component data that doesn't belong in every instance.
+// Using vector instead of unordered_map for both because:
+// - Much lower memory overhead (8 bytes per entry vs 20+ for unordered_map)
+// - Linear search is fine for small n (typically < 5 entries)
+// - These are rarely accessed (setup only or error cases only)
+
+// Component error messages - only stores messages for failed components
+// Lazy allocated since most configs have zero failures
+// Note: We don't clear this vector because:
+// 1. Components are never destroyed in ESPHome
+// 2. Failed components remain failed (no recovery mechanism)
+// 3. Memory usage is minimal (only failures with custom messages are stored)
+
+// Using namespace-scope static to avoid guard variables (saves 16 bytes total)
+// This is safe because ESPHome is single-threaded during initialization
+namespace {
+// Error messages for failed components
+// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
+std::unique_ptr>> component_error_messages;
+// Setup priority overrides - freed after setup completes
+// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
+std::unique_ptr>> setup_priority_overrides;
+}  // namespace
+
 namespace setup_priority {
 
 const float BUS = 1000.0f;
@@ -30,17 +56,18 @@ const float LATE = -100.0f;
 
 }  // namespace setup_priority
 
-// Component state uses bits 0-1 (4 states)
-const uint8_t COMPONENT_STATE_MASK = 0x03;
+// Component state uses bits 0-2 (8 states, 5 used)
+const uint8_t COMPONENT_STATE_MASK = 0x07;
 const uint8_t COMPONENT_STATE_CONSTRUCTION = 0x00;
 const uint8_t COMPONENT_STATE_SETUP = 0x01;
 const uint8_t COMPONENT_STATE_LOOP = 0x02;
 const uint8_t COMPONENT_STATE_FAILED = 0x03;
-// Status LED uses bits 2-3
-const uint8_t STATUS_LED_MASK = 0x0C;
+const uint8_t COMPONENT_STATE_LOOP_DONE = 0x04;
+// Status LED uses bits 3-4
+const uint8_t STATUS_LED_MASK = 0x18;
 const uint8_t STATUS_LED_OK = 0x00;
-const uint8_t STATUS_LED_WARNING = 0x04;  // Bit 2
-const uint8_t STATUS_LED_ERROR = 0x08;    // Bit 3
+const uint8_t STATUS_LED_WARNING = 0x08;  // Bit 3
+const uint8_t STATUS_LED_ERROR = 0x10;    // Bit 4
 
 const uint16_t WARN_IF_BLOCKING_OVER_MS = 50U;       ///< Initial blocking time allowed without warning
 const uint16_t WARN_IF_BLOCKING_INCREMENT_MS = 10U;  ///< How long the blocking time must be larger to warn again
@@ -59,10 +86,18 @@ void Component::set_interval(const std::string &name, uint32_t interval, std::fu
   App.scheduler.set_interval(this, name, interval, std::move(f));
 }
 
+void Component::set_interval(const char *name, uint32_t interval, std::function &&f) {  // NOLINT
+  App.scheduler.set_interval(this, name, interval, std::move(f));
+}
+
 bool Component::cancel_interval(const std::string &name) {  // NOLINT
   return App.scheduler.cancel_interval(this, name);
 }
 
+bool Component::cancel_interval(const char *name) {  // NOLINT
+  return App.scheduler.cancel_interval(this, name);
+}
+
 void Component::set_retry(const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts,
                           std::function &&f, float backoff_increase_factor) {  // NOLINT
   App.scheduler.set_retry(this, name, initial_wait_time, max_attempts, std::move(f), backoff_increase_factor);
@@ -76,16 +111,34 @@ void Component::set_timeout(const std::string &name, uint32_t timeout, std::func
   App.scheduler.set_timeout(this, name, timeout, std::move(f));
 }
 
+void Component::set_timeout(const char *name, uint32_t timeout, std::function &&f) {  // NOLINT
+  App.scheduler.set_timeout(this, name, timeout, std::move(f));
+}
+
 bool Component::cancel_timeout(const std::string &name) {  // NOLINT
   return App.scheduler.cancel_timeout(this, name);
 }
 
+bool Component::cancel_timeout(const char *name) {  // NOLINT
+  return App.scheduler.cancel_timeout(this, name);
+}
+
 void Component::call_loop() { this->loop(); }
 void Component::call_setup() { this->setup(); }
 void Component::call_dump_config() {
   this->dump_config();
   if (this->is_failed()) {
-    ESP_LOGE(TAG, "  Component %s is marked FAILED: %s", this->get_component_source(), this->error_message_.c_str());
+    // Look up error message from global vector
+    const char *error_msg = "unspecified";
+    if (component_error_messages) {
+      for (const auto &pair : *component_error_messages) {
+        if (pair.first == this) {
+          error_msg = pair.second;
+          break;
+        }
+      }
+    }
+    ESP_LOGE(TAG, "  Component %s is marked FAILED: %s", this->get_component_source(), error_msg);
   }
 }
 
@@ -112,6 +165,9 @@ void Component::call() {
     case COMPONENT_STATE_FAILED:  // NOLINT(bugprone-branch-clone)
       // State failed: Do nothing
       break;
+    case COMPONENT_STATE_LOOP_DONE:  // NOLINT(bugprone-branch-clone)
+      // State loop done: Do nothing, component has finished its work
+      break;
     default:
       break;
   }
@@ -135,14 +191,45 @@ bool Component::should_warn_of_blocking(uint32_t blocking_time) {
   return false;
 }
 void Component::mark_failed() {
-  ESP_LOGE(TAG, "Component %s was marked as failed.", this->get_component_source());
+  ESP_LOGE(TAG, "Component %s was marked as failed", this->get_component_source());
   this->component_state_ &= ~COMPONENT_STATE_MASK;
   this->component_state_ |= COMPONENT_STATE_FAILED;
   this->status_set_error();
+  // Also remove from loop since failed components shouldn't loop
+  App.disable_component_loop_(this);
+}
+void Component::disable_loop() {
+  if ((this->component_state_ & COMPONENT_STATE_MASK) != COMPONENT_STATE_LOOP_DONE) {
+    ESP_LOGVV(TAG, "%s loop disabled", this->get_component_source());
+    this->component_state_ &= ~COMPONENT_STATE_MASK;
+    this->component_state_ |= COMPONENT_STATE_LOOP_DONE;
+    App.disable_component_loop_(this);
+  }
+}
+void Component::enable_loop() {
+  if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE) {
+    ESP_LOGVV(TAG, "%s loop enabled", this->get_component_source());
+    this->component_state_ &= ~COMPONENT_STATE_MASK;
+    this->component_state_ |= COMPONENT_STATE_LOOP;
+    App.enable_component_loop_(this);
+  }
+}
+void IRAM_ATTR HOT Component::enable_loop_soon_any_context() {
+  // This method is thread and ISR-safe because:
+  // 1. Only performs simple assignments to volatile variables (atomic on all platforms)
+  // 2. No read-modify-write operations that could be interrupted
+  // 3. No memory allocation, object construction, or function calls
+  // 4. IRAM_ATTR ensures code is in IRAM, not flash (required for ISR execution)
+  // 5. Components are never destroyed, so no use-after-free concerns
+  // 6. App is guaranteed to be initialized before any ISR could fire
+  // 7. Multiple ISR/thread calls are safe - just sets the same flags to true
+  // 8. Race condition with main loop is handled by clearing flag before processing
+  this->pending_enable_loop_ = true;
+  App.has_pending_enable_loop_requests_ = true;
 }
 void Component::reset_to_construction_state() {
   if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) {
-    ESP_LOGI(TAG, "Component %s is being reset to construction state.", this->get_component_source());
+    ESP_LOGI(TAG, "Component %s is being reset to construction state", this->get_component_source());
     this->component_state_ &= ~COMPONENT_STATE_MASK;
     this->component_state_ |= COMPONENT_STATE_CONSTRUCTION;
     // Clear error status when resetting
@@ -153,7 +240,7 @@ bool Component::is_in_loop_state() const {
   return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP;
 }
 void Component::defer(std::function &&f) {  // NOLINT
-  App.scheduler.set_timeout(this, "", 0, std::move(f));
+  App.scheduler.set_timeout(this, static_cast(nullptr), 0, std::move(f));
 }
 bool Component::cancel_defer(const std::string &name) {  // NOLINT
   return App.scheduler.cancel_timeout(this, name);
@@ -161,6 +248,9 @@ bool Component::cancel_defer(const std::string &name) {  // NOLINT
 void Component::defer(const std::string &name, std::function &&f) {  // NOLINT
   App.scheduler.set_timeout(this, name, 0, std::move(f));
 }
+void Component::defer(const char *name, std::function &&f) {  // NOLINT
+  App.scheduler.set_timeout(this, name, 0, std::move(f));
+}
 void Component::set_timeout(uint32_t timeout, std::function &&f) {  // NOLINT
   App.scheduler.set_timeout(this, "", timeout, std::move(f));
 }
@@ -193,8 +283,21 @@ void Component::status_set_error(const char *message) {
   this->component_state_ |= STATUS_LED_ERROR;
   App.app_state_ |= STATUS_LED_ERROR;
   ESP_LOGE(TAG, "Component %s set Error flag: %s", this->get_component_source(), message);
-  if (strcmp(message, "unspecified") != 0)
-    this->error_message_ = message;
+  if (strcmp(message, "unspecified") != 0) {
+    // Lazy allocate the error messages vector if needed
+    if (!component_error_messages) {
+      component_error_messages = std::make_unique>>();
+    }
+    // Check if this component already has an error message
+    for (auto &pair : *component_error_messages) {
+      if (pair.first == this) {
+        pair.second = message;
+        return;
+      }
+    }
+    // Add new error message
+    component_error_messages->emplace_back(this, message);
+  }
 }
 void Component::status_clear_warning() {
   if ((this->component_state_ & STATUS_LED_WARNING) == 0)
@@ -218,11 +321,36 @@ void Component::status_momentary_error(const std::string &name, uint32_t length)
 }
 void Component::dump_config() {}
 float Component::get_actual_setup_priority() const {
-  if (std::isnan(this->setup_priority_override_))
-    return this->get_setup_priority();
-  return this->setup_priority_override_;
+  // Check if there's an override in the global vector
+  if (setup_priority_overrides) {
+    // Linear search is fine for small n (typically < 5 overrides)
+    for (const auto &pair : *setup_priority_overrides) {
+      if (pair.first == this) {
+        return pair.second;
+      }
+    }
+  }
+  return this->get_setup_priority();
+}
+void Component::set_setup_priority(float priority) {
+  // Lazy allocate the vector if needed
+  if (!setup_priority_overrides) {
+    setup_priority_overrides = std::make_unique>>();
+    // Reserve some space to avoid reallocations (most configs have < 10 overrides)
+    setup_priority_overrides->reserve(10);
+  }
+
+  // Check if this component already has an override
+  for (auto &pair : *setup_priority_overrides) {
+    if (pair.first == this) {
+      pair.second = priority;
+      return;
+    }
+  }
+
+  // Add new override
+  setup_priority_overrides->emplace_back(this, priority);
 }
-void Component::set_setup_priority(float priority) { this->setup_priority_override_ = priority; }
 
 bool Component::has_overridden_loop() const {
 #if defined(USE_HOST) || defined(CLANG_TIDY)
@@ -275,8 +403,8 @@ uint32_t WarnIfComponentBlockingGuard::finish() {
   }
   if (should_warn) {
     const char *src = component_ == nullptr ? "" : component_->get_component_source();
-    ESP_LOGW(TAG, "Component %s took a long time for an operation (%" PRIu32 " ms).", src, blocking_time);
-    ESP_LOGW(TAG, "Components should block for at most 30 ms.");
+    ESP_LOGW(TAG, "Component %s took a long time for an operation (%" PRIu32 " ms)", src, blocking_time);
+    ESP_LOGW(TAG, "Components should block for at most 30 ms");
   }
 
   return curr_time;
@@ -284,4 +412,9 @@ uint32_t WarnIfComponentBlockingGuard::finish() {
 
 WarnIfComponentBlockingGuard::~WarnIfComponentBlockingGuard() {}
 
+void clear_setup_priority_overrides() {
+  // Free the setup priority map completely
+  setup_priority_overrides.reset();
+}
+
 }  // namespace esphome
diff --git a/esphome/core/component.h b/esphome/core/component.h
index d05a965034..3734473a02 100644
--- a/esphome/core/component.h
+++ b/esphome/core/component.h
@@ -58,6 +58,7 @@ extern const uint8_t COMPONENT_STATE_CONSTRUCTION;
 extern const uint8_t COMPONENT_STATE_SETUP;
 extern const uint8_t COMPONENT_STATE_LOOP;
 extern const uint8_t COMPONENT_STATE_FAILED;
+extern const uint8_t COMPONENT_STATE_LOOP_DONE;
 extern const uint8_t STATUS_LED_MASK;
 extern const uint8_t STATUS_LED_OK;
 extern const uint8_t STATUS_LED_WARNING;
@@ -150,6 +151,47 @@ class Component {
     this->mark_failed();
   }
 
+  /** Disable this component's loop. The loop() method will no longer be called.
+   *
+   * This is useful for components that only need to run for a certain period of time
+   * or when inactive, saving CPU cycles.
+   *
+   * @note Components should call this->disable_loop() on themselves, not on other components.
+   *       This ensures the component's state is properly updated along with the loop partition.
+   */
+  void disable_loop();
+
+  /** Enable this component's loop. The loop() method will be called normally.
+   *
+   * This is useful for components that transition between active and inactive states
+   * and need to re-enable their loop() method when becoming active again.
+   *
+   * @note Components should call this->enable_loop() on themselves, not on other components.
+   *       This ensures the component's state is properly updated along with the loop partition.
+   */
+  void enable_loop();
+
+  /** Thread and ISR-safe version of enable_loop() that can be called from any context.
+   *
+   * This method defers the actual enable via enable_pending_loops_ to the main loop,
+   * making it safe to call from ISR handlers, timer callbacks, other threads,
+   * or any interrupt context.
+   *
+   * @note The actual loop enabling will happen on the next main loop iteration.
+   * @note Only one pending enable request is tracked per component.
+   * @note There is no disable_loop_soon_any_context() on purpose - it would race
+   *       against enable calls and synchronization would get too complex
+   *       to provide a safe version that would work for each component.
+   *
+   *       Use disable_loop() from the main thread only.
+   *
+   *       If you need to disable the loop from ISR, carefully implement
+   *       it in the component itself, with an ISR safe approach, and call
+   *       disable_loop() in its next ::loop() iteration. Implementations
+   *       will need to carefully consider all possible race conditions.
+   */
+  void enable_loop_soon_any_context();
+
   bool is_failed() const;
 
   bool is_ready() const;
@@ -218,6 +260,22 @@ class Component {
    */
   void set_interval(const std::string &name, uint32_t interval, std::function &&f);  // NOLINT
 
+  /** Set an interval function with a const char* name.
+   *
+   * IMPORTANT: The provided name pointer must remain valid for the lifetime of the scheduler item.
+   * This means the name should be:
+   *   - A string literal (e.g., "update")
+   *   - A static const char* variable
+   *   - A pointer with lifetime >= the scheduled task
+   *
+   * For dynamic strings, use the std::string overload instead.
+   *
+   * @param name The identifier for this interval function (must have static lifetime)
+   * @param interval The interval in ms
+   * @param f The function to call
+   */
+  void set_interval(const char *name, uint32_t interval, std::function &&f);  // NOLINT
+
   void set_interval(uint32_t interval, std::function &&f);  // NOLINT
 
   /** Cancel an interval function.
@@ -226,6 +284,7 @@ class Component {
    * @return Whether an interval functions was deleted.
    */
   bool cancel_interval(const std::string &name);  // NOLINT
+  bool cancel_interval(const char *name);         // NOLINT
 
   /** Set an retry function with a unique name. Empty name means no cancelling possible.
    *
@@ -286,6 +345,22 @@ class Component {
    */
   void set_timeout(const std::string &name, uint32_t timeout, std::function &&f);  // NOLINT
 
+  /** Set a timeout function with a const char* name.
+   *
+   * IMPORTANT: The provided name pointer must remain valid for the lifetime of the scheduler item.
+   * This means the name should be:
+   *   - A string literal (e.g., "init")
+   *   - A static const char* variable
+   *   - A pointer with lifetime >= the timeout duration
+   *
+   * For dynamic strings, use the std::string overload instead.
+   *
+   * @param name The identifier for this timeout function (must have static lifetime)
+   * @param timeout The timeout in ms
+   * @param f The function to call
+   */
+  void set_timeout(const char *name, uint32_t timeout, std::function &&f);  // NOLINT
+
   void set_timeout(uint32_t timeout, std::function &&f);  // NOLINT
 
   /** Cancel a timeout function.
@@ -294,6 +369,7 @@ class Component {
    * @return Whether a timeout functions was deleted.
    */
   bool cancel_timeout(const std::string &name);  // NOLINT
+  bool cancel_timeout(const char *name);         // NOLINT
 
   /** Defer a callback to the next loop() call.
    *
@@ -304,22 +380,37 @@ class Component {
    */
   void defer(const std::string &name, std::function &&f);  // NOLINT
 
+  /** Defer a callback to the next loop() call with a const char* name.
+   *
+   * IMPORTANT: The provided name pointer must remain valid for the lifetime of the deferred task.
+   * This means the name should be:
+   *   - A string literal (e.g., "update")
+   *   - A static const char* variable
+   *   - A pointer with lifetime >= the deferred execution
+   *
+   * For dynamic strings, use the std::string overload instead.
+   *
+   * @param name The name of the defer function (must have static lifetime)
+   * @param f The callback
+   */
+  void defer(const char *name, std::function &&f);  // NOLINT
+
   /// Defer a callback to the next loop() call.
   void defer(std::function &&f);  // NOLINT
 
   /// Cancel a defer callback using the specified name, name must not be empty.
   bool cancel_defer(const std::string &name);  // NOLINT
 
+  // Ordered for optimal packing on 32-bit systems
+  const char *component_source_{nullptr};
+  uint16_t warn_if_blocking_over_{WARN_IF_BLOCKING_OVER_MS};  ///< Warn if blocked for this many ms (max 65.5s)
   /// State of this component - each bit has a purpose:
   /// Bits 0-1: Component state (0x00=CONSTRUCTION, 0x01=SETUP, 0x02=LOOP, 0x03=FAILED)
   /// Bit 2: STATUS_LED_WARNING
   /// Bit 3: STATUS_LED_ERROR
   /// Bits 4-7: Unused - reserved for future expansion (50% of the bits are free)
   uint8_t component_state_{0x00};
-  float setup_priority_override_{NAN};
-  const char *component_source_{nullptr};
-  uint16_t warn_if_blocking_over_{WARN_IF_BLOCKING_OVER_MS};  ///< Warn if blocked for this many ms (max 65.5s)
-  std::string error_message_{};
+  volatile bool pending_enable_loop_{false};  ///< ISR-safe flag for enable_loop_soon_any_context
 };
 
 /** This class simplifies creating components that periodically check a state.
@@ -381,4 +472,7 @@ class WarnIfComponentBlockingGuard {
   Component *component_;
 };
 
+// Function to clear setup priority overrides after all components are set up
+void clear_setup_priority_overrides();
+
 }  // namespace esphome
diff --git a/esphome/core/component_iterator.cpp b/esphome/core/component_iterator.cpp
index da593340c1..aab5c2a72d 100644
--- a/esphome/core/component_iterator.cpp
+++ b/esphome/core/component_iterator.cpp
@@ -158,16 +158,16 @@ void ComponentIterator::advance() {
       }
       break;
 #endif
-#ifdef USE_ESP32_CAMERA
+#ifdef USE_CAMERA
     case IteratorState::CAMERA:
-      if (esp32_camera::global_esp32_camera == nullptr) {
+      if (camera::Camera::instance() == nullptr) {
         advance_platform = true;
       } else {
-        if (esp32_camera::global_esp32_camera->is_internal() && !this->include_internal_) {
+        if (camera::Camera::instance()->is_internal() && !this->include_internal_) {
           advance_platform = success = true;
           break;
         } else {
-          advance_platform = success = this->on_camera(esp32_camera::global_esp32_camera);
+          advance_platform = success = this->on_camera(camera::Camera::instance());
         }
       }
       break;
@@ -375,7 +375,7 @@ void ComponentIterator::advance() {
   }
 
   if (advance_platform) {
-    this->state_ = static_cast(static_cast(this->state_) + 1);
+    this->state_ = static_cast(static_cast(this->state_) + 1);
     this->at_ = 0;
   } else if (success) {
     this->at_++;
@@ -386,8 +386,8 @@ bool ComponentIterator::on_begin() { return true; }
 #ifdef USE_API
 bool ComponentIterator::on_service(api::UserServiceDescriptor *service) { return true; }
 #endif
-#ifdef USE_ESP32_CAMERA
-bool ComponentIterator::on_camera(esp32_camera::ESP32Camera *camera) { return true; }
+#ifdef USE_CAMERA
+bool ComponentIterator::on_camera(camera::Camera *camera) { return true; }
 #endif
 #ifdef USE_MEDIA_PLAYER
 bool ComponentIterator::on_media_player(media_player::MediaPlayer *media_player) { return true; }
diff --git a/esphome/core/component_iterator.h b/esphome/core/component_iterator.h
index 9e187f6c57..eda786be7f 100644
--- a/esphome/core/component_iterator.h
+++ b/esphome/core/component_iterator.h
@@ -4,8 +4,8 @@
 #include "esphome/core/controller.h"
 #include "esphome/core/helpers.h"
 
-#ifdef USE_ESP32_CAMERA
-#include "esphome/components/esp32_camera/esp32_camera.h"
+#ifdef USE_CAMERA
+#include "esphome/components/camera/camera.h"
 #endif
 
 namespace esphome {
@@ -48,8 +48,8 @@ class ComponentIterator {
 #ifdef USE_API
   virtual bool on_service(api::UserServiceDescriptor *service);
 #endif
-#ifdef USE_ESP32_CAMERA
-  virtual bool on_camera(esp32_camera::ESP32Camera *camera);
+#ifdef USE_CAMERA
+  virtual bool on_camera(camera::Camera *camera);
 #endif
 #ifdef USE_CLIMATE
   virtual bool on_climate(climate::Climate *climate) = 0;
@@ -93,7 +93,9 @@ class ComponentIterator {
   virtual bool on_end();
 
  protected:
-  enum class IteratorState {
+  // Iterates over all ESPHome entities (sensors, switches, lights, etc.)
+  // Supports up to 256 entity types and up to 65,535 entities of each type
+  enum class IteratorState : uint8_t {
     NONE = 0,
     BEGIN,
 #ifdef USE_BINARY_SENSOR
@@ -123,7 +125,7 @@ class ComponentIterator {
 #ifdef USE_API
     SERVICE,
 #endif
-#ifdef USE_ESP32_CAMERA
+#ifdef USE_CAMERA
     CAMERA,
 #endif
 #ifdef USE_CLIMATE
@@ -167,7 +169,7 @@ class ComponentIterator {
 #endif
     MAX,
   } state_{IteratorState::NONE};
-  size_t at_{0};
+  uint16_t at_{0};  // Supports up to 65,535 entities per type
   bool include_internal_{false};
 };
 
diff --git a/esphome/core/config.py b/esphome/core/config.py
index c407e1c11a..f73369f28f 100644
--- a/esphome/core/config.py
+++ b/esphome/core/config.py
@@ -1,18 +1,25 @@
+from __future__ import annotations
+
 import logging
 import os
 from pathlib import Path
 
-from esphome import automation
+from esphome import automation, core
 import esphome.codegen as cg
+from esphome.config_helpers import filter_source_files_from_platform
 import esphome.config_validation as cv
 from esphome.const import (
     CONF_AREA,
+    CONF_AREA_ID,
+    CONF_AREAS,
     CONF_BUILD_PATH,
     CONF_COMMENT,
     CONF_COMPILE_PROCESS_LIMIT,
     CONF_DEBUG_SCHEDULER,
+    CONF_DEVICES,
     CONF_ESPHOME,
     CONF_FRIENDLY_NAME,
+    CONF_ID,
     CONF_INCLUDES,
     CONF_LIBRARIES,
     CONF_MIN_VERSION,
@@ -29,10 +36,17 @@ from esphome.const import (
     CONF_TRIGGER_ID,
     CONF_VERSION,
     KEY_CORE,
+    PlatformFramework,
     __version__ as ESPHOME_VERSION,
 )
 from esphome.core import CORE, coroutine_with_priority
-from esphome.helpers import copy_file_if_changed, get_str_env, walk_files
+from esphome.helpers import (
+    copy_file_if_changed,
+    fnv1a_32bit_hash,
+    get_str_env,
+    walk_files,
+)
+from esphome.types import ConfigType
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -48,7 +62,8 @@ LoopTrigger = cg.esphome_ns.class_(
 ProjectUpdateTrigger = cg.esphome_ns.class_(
     "ProjectUpdateTrigger", cg.Component, automation.Trigger.template(cg.std_string)
 )
-
+Device = cg.esphome_ns.class_("Device")
+Area = cg.esphome_ns.class_("Area")
 
 VALID_INCLUDE_EXTS = {".h", ".hpp", ".tcc", ".ino", ".cpp", ".c"}
 
@@ -71,6 +86,56 @@ def validate_hostname(config):
     return config
 
 
+def validate_ids_and_references(config: ConfigType) -> ConfigType:
+    """Validate that there are no hash collisions between IDs and that area_id references are valid.
+
+    This validation is critical because we use 32-bit hashes for performance on microcontrollers.
+    By detecting collisions at compile time, we prevent any runtime issues while maintaining
+    optimal performance on 32-bit platforms. In practice, with typical deployments having only
+    a handful of areas and devices, hash collisions are virtually impossible.
+    """
+
+    # Helper to check hash collisions
+    def check_hash_collision(
+        id_obj: core.ID,
+        hash_dict: dict[int, str],
+        item_type: str,
+        path: list[str | int],
+    ) -> None:
+        hash_val: int = fnv1a_32bit_hash(id_obj.id)
+        if hash_val in hash_dict and hash_dict[hash_val] != id_obj.id:
+            raise cv.Invalid(
+                f"{item_type} ID '{id_obj.id}' with hash {hash_val} collides with "
+                f"existing {item_type.lower()} ID '{hash_dict[hash_val]}'",
+                path=path,
+            )
+        hash_dict[hash_val] = id_obj.id
+
+    # Collect all areas
+    all_areas: list[dict[str, str | core.ID]] = []
+    if CONF_AREA in config:
+        all_areas.append(config[CONF_AREA])
+    all_areas.extend(config[CONF_AREAS])
+
+    # Validate area hash collisions and collect IDs
+    area_hashes: dict[int, str] = {}
+    area_ids: set[str] = set()
+    for area in all_areas:
+        area_id: core.ID = area[CONF_ID]
+        check_hash_collision(area_id, area_hashes, "Area", [CONF_AREAS, area_id.id])
+        area_ids.add(area_id.id)
+
+    # Validate device hash collisions and area references
+    device_hashes: dict[int, str] = {}
+    for device in config[CONF_DEVICES]:
+        device_id: core.ID = device[CONF_ID]
+        check_hash_collision(
+            device_id, device_hashes, "Device", [CONF_DEVICES, device_id.id]
+        )
+
+    return config
+
+
 def valid_include(value):
     # Look for "<...>" includes
     if value.startswith("<") and value.endswith(">"):
@@ -111,13 +176,32 @@ if "ESPHOME_DEFAULT_COMPILE_PROCESS_LIMIT" in os.environ:
 else:
     _compile_process_limit_default = cv.UNDEFINED
 
+AREA_SCHEMA = cv.Schema(
+    {
+        cv.GenerateID(CONF_ID): cv.declare_id(Area),
+        cv.Required(CONF_NAME): cv.string,
+    }
+)
+
+DEVICE_SCHEMA = cv.Schema(
+    {
+        cv.GenerateID(CONF_ID): cv.declare_id(Device),
+        cv.Required(CONF_NAME): cv.string,
+        cv.Optional(CONF_AREA_ID): cv.use_id(Area),
+    }
+)
+
+
+def validate_area_config(config: dict | str) -> dict[str, str | core.ID]:
+    return cv.maybe_simple_value(AREA_SCHEMA, key=CONF_NAME)(config)
+
 
 CONFIG_SCHEMA = cv.All(
     cv.Schema(
         {
             cv.Required(CONF_NAME): cv.valid_name,
             cv.Optional(CONF_FRIENDLY_NAME, ""): cv.string,
-            cv.Optional(CONF_AREA, ""): cv.string,
+            cv.Optional(CONF_AREA): validate_area_config,
             cv.Optional(CONF_COMMENT): cv.string,
             cv.Required(CONF_BUILD_PATH): cv.string,
             cv.Optional(CONF_PLATFORMIO_OPTIONS, default={}): cv.Schema(
@@ -167,11 +251,17 @@ CONFIG_SCHEMA = cv.All(
             cv.Optional(
                 CONF_COMPILE_PROCESS_LIMIT, default=_compile_process_limit_default
             ): cv.int_range(min=1, max=get_usable_cpu_count()),
+            cv.Optional(CONF_AREAS, default=[]): cv.ensure_list(AREA_SCHEMA),
+            cv.Optional(CONF_DEVICES, default=[]): cv.ensure_list(DEVICE_SCHEMA),
         }
     ),
     validate_hostname,
 )
 
+
+FINAL_VALIDATE_SCHEMA = cv.All(validate_ids_and_references)
+
+
 PRELOAD_CONFIG_SCHEMA = cv.Schema(
     {
         cv.Required(CONF_NAME): cv.valid_name,
@@ -336,7 +426,7 @@ async def _add_platform_reserves() -> None:
 
 
 @coroutine_with_priority(100.0)
-async def to_code(config):
+async def to_code(config: ConfigType) -> None:
     cg.add_global(cg.global_ns.namespace("esphome").using)
     # These can be used by user lambdas, put them to default scope
     cg.add_global(cg.RawExpression("using std::isnan"))
@@ -347,7 +437,6 @@ async def to_code(config):
         cg.App.pre_setup(
             config[CONF_NAME],
             config[CONF_FRIENDLY_NAME],
-            config[CONF_AREA],
             config.get(CONF_COMMENT, ""),
             cg.RawExpression('__DATE__ ", " __TIME__'),
             config[CONF_NAME_ADD_MAC_SUFFIX],
@@ -417,3 +506,63 @@ async def to_code(config):
 
     if config[CONF_PLATFORMIO_OPTIONS]:
         CORE.add_job(_add_platformio_options, config[CONF_PLATFORMIO_OPTIONS])
+
+    # Process areas
+    all_areas: list[dict[str, str | core.ID]] = []
+    if CONF_AREA in config:
+        all_areas.append(config[CONF_AREA])
+    all_areas.extend(config[CONF_AREAS])
+
+    if all_areas:
+        cg.add(cg.RawStatement(f"App.reserve_area({len(all_areas)});"))
+        cg.add_define("USE_AREAS")
+
+        for area_conf in all_areas:
+            area_id: core.ID = area_conf[CONF_ID]
+            area_id_hash: int = fnv1a_32bit_hash(area_id.id)
+            area_name: str = area_conf[CONF_NAME]
+
+            area_var = cg.new_Pvariable(area_id)
+            cg.add(area_var.set_area_id(area_id_hash))
+            cg.add(area_var.set_name(area_name))
+            cg.add(cg.App.register_area(area_var))
+
+    # Process devices
+    devices: list[dict[str, str | core.ID]] = config[CONF_DEVICES]
+    if not devices:
+        return
+
+    # Reserve space for devices
+    cg.add(cg.RawStatement(f"App.reserve_device({len(devices)});"))
+    cg.add_define("USE_DEVICES")
+
+    # Process each device
+    for dev_conf in devices:
+        device_id: core.ID = dev_conf[CONF_ID]
+        device_id_hash = fnv1a_32bit_hash(device_id.id)
+        device_name: str = dev_conf[CONF_NAME]
+
+        dev = cg.new_Pvariable(device_id)
+        cg.add(dev.set_device_id(device_id_hash))
+        cg.add(dev.set_name(device_name))
+
+        # Set area if specified
+        if CONF_AREA_ID in dev_conf:
+            area_id: core.ID = dev_conf[CONF_AREA_ID]
+            area_id_hash = fnv1a_32bit_hash(area_id.id)
+            cg.add(dev.set_area_id(area_id_hash))
+
+        cg.add(cg.App.register_device(dev))
+
+
+# Platform-specific source files for core
+FILTER_SOURCE_FILES = filter_source_files_from_platform(
+    {
+        "ring_buffer.cpp": {
+            PlatformFramework.ESP32_ARDUINO,
+            PlatformFramework.ESP32_IDF,
+        },
+        # Note: lock_free_queue.h and event_pool.h are header files and don't need to be filtered
+        # as they are only included when needed by the preprocessor
+    }
+)
diff --git a/esphome/core/controller.cpp b/esphome/core/controller.cpp
index d6d98a4316..f7ff5a9734 100644
--- a/esphome/core/controller.cpp
+++ b/esphome/core/controller.cpp
@@ -7,8 +7,10 @@ namespace esphome {
 void Controller::setup_controller(bool include_internal) {
 #ifdef USE_BINARY_SENSOR
   for (auto *obj : App.get_binary_sensors()) {
-    if (include_internal || !obj->is_internal())
-      obj->add_on_state_callback([this, obj](bool state) { this->on_binary_sensor_update(obj, state); });
+    if (include_internal || !obj->is_internal()) {
+      obj->add_full_state_callback(
+          [this, obj](optional previous, optional state) { this->on_binary_sensor_update(obj); });
+    }
   }
 #endif
 #ifdef USE_FAN
diff --git a/esphome/core/controller.h b/esphome/core/controller.h
index 39e0b2ba26..1a5b9ea6b4 100644
--- a/esphome/core/controller.h
+++ b/esphome/core/controller.h
@@ -71,7 +71,7 @@ class Controller {
  public:
   void setup_controller(bool include_internal = false);
 #ifdef USE_BINARY_SENSOR
-  virtual void on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state){};
+  virtual void on_binary_sensor_update(binary_sensor::BinarySensor *obj){};
 #endif
 #ifdef USE_FAN
   virtual void on_fan_update(fan::Fan *obj){};
diff --git a/esphome/core/datatypes.h b/esphome/core/datatypes.h
index 5356be6b52..4929518387 100644
--- a/esphome/core/datatypes.h
+++ b/esphome/core/datatypes.h
@@ -11,7 +11,7 @@ namespace internal {
 /// Wrapper class for memory using big endian data layout, transparently converting it to native order.
 template class BigEndianLayout {
  public:
-  constexpr14 operator T() { return convert_big_endian(val_); }
+  constexpr operator T() { return convert_big_endian(val_); }
 
  private:
   T val_;
@@ -20,7 +20,7 @@ template class BigEndianLayout {
 /// Wrapper class for memory using big endian data layout, transparently converting it to native order.
 template class LittleEndianLayout {
  public:
-  constexpr14 operator T() { return convert_little_endian(val_); }
+  constexpr operator T() { return convert_little_endian(val_); }
 
  private:
   T val_;
diff --git a/esphome/core/defines.h b/esphome/core/defines.h
index f7a937c28d..d73009436b 100644
--- a/esphome/core/defines.h
+++ b/esphome/core/defines.h
@@ -20,8 +20,10 @@
 
 // Feature flags
 #define USE_ALARM_CONTROL_PANEL
+#define USE_AREAS
 #define USE_BINARY_SENSOR
 #define USE_BUTTON
+#define USE_CAMERA
 #define USE_CLIMATE
 #define USE_COVER
 #define USE_DATETIME
@@ -29,7 +31,9 @@
 #define USE_DATETIME_DATETIME
 #define USE_DATETIME_TIME
 #define USE_DEEP_SLEEP
+#define USE_DEVICES
 #define USE_DISPLAY
+#define USE_ENTITY_ICON
 #define USE_ESP32_IMPROV_STATE_CALLBACK
 #define USE_EVENT
 #define USE_FAN
@@ -84,6 +88,7 @@
 #define USE_SELECT
 #define USE_SENSOR
 #define USE_STATUS_LED
+#define USE_STATUS_SENSOR
 #define USE_SWITCH
 #define USE_TEXT
 #define USE_TEXT_SENSOR
@@ -99,8 +104,11 @@
 #define USE_AUDIO_FLAC_SUPPORT
 #define USE_AUDIO_MP3_SUPPORT
 #define USE_API
+#define USE_API_CLIENT_CONNECTED_TRIGGER
+#define USE_API_CLIENT_DISCONNECTED_TRIGGER
 #define USE_API_NOISE
 #define USE_API_PLAINTEXT
+#define USE_API_YAML_SERVICES
 #define USE_MD5
 #define USE_MQTT
 #define USE_NETWORK
@@ -111,6 +119,7 @@
 #define USE_OTA_PASSWORD
 #define USE_OTA_STATE_CALLBACK
 #define USE_OTA_VERSION 2
+#define USE_TIME_TIMEZONE
 #define USE_WIFI
 #define USE_WIFI_AP
 #define USE_WIREGUARD
@@ -130,12 +139,14 @@
 
 // ESP32-specific feature flags
 #ifdef USE_ESP32
+#define USE_ESPHOME_TASK_LOG_BUFFER
+
 #define USE_BLUETOOTH_PROXY
 #define USE_CAPTIVE_PORTAL
 #define USE_ESP32_BLE
 #define USE_ESP32_BLE_CLIENT
 #define USE_ESP32_BLE_SERVER
-#define USE_ESP32_CAMERA
+#define USE_I2C
 #define USE_IMPROV
 #define USE_MICROPHONE
 #define USE_PSRAM
@@ -145,11 +156,13 @@
 #define USE_SPI
 #define USE_VOICE_ASSISTANT
 #define USE_WEBSERVER
+#define USE_WEBSERVER_OTA
 #define USE_WEBSERVER_PORT 80  // NOLINT
+#define USE_WEBSERVER_SORTING
 #define USE_WIFI_11KV_SUPPORT
 
 #ifdef USE_ARDUINO
-#define USE_ARDUINO_VERSION_CODE VERSION_CODE(2, 0, 5)
+#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 1, 3)
 #define USE_ETHERNET
 #endif
 
@@ -179,6 +192,7 @@
 #define USE_CAPTIVE_PORTAL
 #define USE_ESP8266_PREFERENCES_FLASH
 #define USE_HTTP_REQUEST_ESP8266_HTTPS
+#define USE_I2C
 #define USE_SOCKET_IMPL_LWIP_TCP
 
 #define USE_SPI
@@ -195,6 +209,7 @@
 
 #ifdef USE_RP2040
 #define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 0)
+#define USE_I2C
 #define USE_LOGGER_USB_CDC
 #define USE_SOCKET_IMPL_LWIP_TCP
 #define USE_SPI
diff --git a/esphome/core/device.h b/esphome/core/device.h
new file mode 100644
index 0000000000..3d0d1e7c23
--- /dev/null
+++ b/esphome/core/device.h
@@ -0,0 +1,20 @@
+#pragma once
+
+namespace esphome {
+
+class Device {
+ public:
+  void set_device_id(uint32_t device_id) { this->device_id_ = device_id; }
+  uint32_t get_device_id() { return this->device_id_; }
+  void set_name(const char *name) { this->name_ = name; }
+  const char *get_name() { return this->name_; }
+  void set_area_id(uint32_t area_id) { this->area_id_ = area_id; }
+  uint32_t get_area_id() { return this->area_id_; }
+
+ protected:
+  uint32_t device_id_{};
+  uint32_t area_id_{};
+  const char *name_ = "";
+};
+
+}  // namespace esphome
diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp
index 791b6615a1..2ea9c77a3e 100644
--- a/esphome/core/entity_base.cpp
+++ b/esphome/core/entity_base.cpp
@@ -11,7 +11,14 @@ const StringRef &EntityBase::get_name() const { return this->name_; }
 void EntityBase::set_name(const char *name) {
   this->name_ = StringRef(name);
   if (this->name_.empty()) {
-    this->name_ = StringRef(App.get_friendly_name());
+#ifdef USE_DEVICES
+    if (this->device_ != nullptr) {
+      this->name_ = StringRef(this->device_->get_name());
+    } else
+#endif
+    {
+      this->name_ = StringRef(App.get_friendly_name());
+    }
     this->flags_.has_own_name = false;
   } else {
     this->flags_.has_own_name = true;
@@ -20,12 +27,22 @@ void EntityBase::set_name(const char *name) {
 
 // Entity Icon
 std::string EntityBase::get_icon() const {
+#ifdef USE_ENTITY_ICON
   if (this->icon_c_str_ == nullptr) {
     return "";
   }
   return this->icon_c_str_;
+#else
+  return "";
+#endif
+}
+void EntityBase::set_icon(const char *icon) {
+#ifdef USE_ENTITY_ICON
+  this->icon_c_str_ = icon;
+#else
+  // No-op when USE_ENTITY_ICON is not defined
+#endif
 }
-void EntityBase::set_icon(const char *icon) { this->icon_c_str_ = icon; }
 
 // Entity Object ID
 std::string EntityBase::get_object_id() const {
@@ -47,19 +64,7 @@ void EntityBase::set_object_id(const char *object_id) {
 }
 
 // Calculate Object ID Hash from Entity Name
-void EntityBase::calc_object_id_() {
-  // Check if `App.get_friendly_name()` is constant or dynamic.
-  if (!this->flags_.has_own_name && App.is_name_add_mac_suffix_enabled()) {
-    // `App.get_friendly_name()` is dynamic.
-    const auto object_id = str_sanitize(str_snake_case(App.get_friendly_name()));
-    // FNV-1 hash
-    this->object_id_hash_ = fnv1_hash(object_id);
-  } else {
-    // `App.get_friendly_name()` is constant.
-    // FNV-1 hash
-    this->object_id_hash_ = fnv1_hash(this->object_id_c_str_);
-  }
-}
+void EntityBase::calc_object_id_() { this->object_id_hash_ = fnv1_hash(this->get_object_id()); }
 
 uint32_t EntityBase::get_object_id_hash() { return this->object_id_hash_; }
 
diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h
index 6b876a9267..00b1264ed0 100644
--- a/esphome/core/entity_base.h
+++ b/esphome/core/entity_base.h
@@ -3,6 +3,12 @@
 #include 
 #include 
 #include "string_ref.h"
+#include "helpers.h"
+#include "log.h"
+
+#ifdef USE_DEVICES
+#include "device.h"
+#endif
 
 namespace esphome {
 
@@ -49,6 +55,17 @@ class EntityBase {
   std::string get_icon() const;
   void set_icon(const char *icon);
 
+#ifdef USE_DEVICES
+  // Get/set this entity's device id
+  uint32_t get_device_id() const {
+    if (this->device_ == nullptr) {
+      return 0;  // No device set, return 0
+    }
+    return this->device_->get_device_id();
+  }
+  void set_device(Device *device) { this->device_ = device; }
+#endif
+
   // Check if this entity has state
   bool has_state() const { return this->flags_.has_state; }
 
@@ -63,8 +80,13 @@ class EntityBase {
 
   StringRef name_;
   const char *object_id_c_str_{nullptr};
+#ifdef USE_ENTITY_ICON
   const char *icon_c_str_{nullptr};
+#endif
   uint32_t object_id_hash_{};
+#ifdef USE_DEVICES
+  Device *device_{};
+#endif
 
   // Bit-packed flags to save memory (1 byte instead of 5)
   struct EntityFlags {
@@ -99,4 +121,58 @@ class EntityBase_UnitOfMeasurement {  // NOLINT(readability-identifier-naming)
   const char *unit_of_measurement_{nullptr};  ///< Unit of measurement override
 };
 
+/**
+ * An entity that has a state.
+ * @tparam T The type of the state
+ */
+template class StatefulEntityBase : public EntityBase {
+ public:
+  virtual bool has_state() const { return this->state_.has_value(); }
+  virtual const T &get_state() const { return this->state_.value(); }
+  virtual T get_state_default(T default_value) const { return this->state_.value_or(default_value); }
+  void invalidate_state() { this->set_state_({}); }
+
+  void add_full_state_callback(std::function previous, optional current)> &&callback) {
+    if (this->full_state_callbacks_ == nullptr)
+      this->full_state_callbacks_ = new CallbackManager previous, optional current)>();  // NOLINT
+    this->full_state_callbacks_->add(std::move(callback));
+  }
+  void add_on_state_callback(std::function &&callback) {
+    if (this->state_callbacks_ == nullptr)
+      this->state_callbacks_ = new CallbackManager();  // NOLINT
+    this->state_callbacks_->add(std::move(callback));
+  }
+
+  void set_trigger_on_initial_state(bool trigger_on_initial_state) {
+    this->trigger_on_initial_state_ = trigger_on_initial_state;
+  }
+
+ protected:
+  optional state_{};
+  /**
+   * Set a new state for this entity. This will trigger callbacks only if the new state is different from the previous.
+   *
+   * @param state The new state.
+   * @return True if the state was changed, false if it was the same as before.
+   */
+  bool set_state_(const optional &state) {
+    if (this->state_ != state) {
+      // call the full state callbacks with the previous and new state
+      if (this->full_state_callbacks_ != nullptr)
+        this->full_state_callbacks_->call(this->state_, state);
+      // trigger legacy callbacks only if the new state is valid and either the trigger on initial state is enabled or
+      // the previous state was valid
+      auto had_state = this->has_state();
+      this->state_ = state;
+      if (this->state_callbacks_ != nullptr && state.has_value() && (this->trigger_on_initial_state_ || had_state))
+        this->state_callbacks_->call(state.value());
+      return true;
+    }
+    return false;
+  }
+  bool trigger_on_initial_state_{true};
+  // callbacks with full state and previous state
+  CallbackManager previous, optional current)> *full_state_callbacks_{};
+  CallbackManager *state_callbacks_{};
+};
 }  // namespace esphome
diff --git a/esphome/core/entity_helpers.py b/esphome/core/entity_helpers.py
index 7f6a9b48ab..a3244856a2 100644
--- a/esphome/core/entity_helpers.py
+++ b/esphome/core/entity_helpers.py
@@ -1,5 +1,119 @@
-from esphome.const import CONF_ID
+from collections.abc import Callable
+import logging
+
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.const import (
+    CONF_DEVICE_ID,
+    CONF_DISABLED_BY_DEFAULT,
+    CONF_ENTITY_CATEGORY,
+    CONF_ICON,
+    CONF_ID,
+    CONF_INTERNAL,
+    CONF_NAME,
+)
+from esphome.core import CORE, ID
+from esphome.cpp_generator import MockObj, add, get_variable
 import esphome.final_validate as fv
+from esphome.helpers import sanitize, snake_case
+from esphome.types import ConfigType
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def get_base_entity_object_id(
+    name: str, friendly_name: str | None, device_name: str | None = None
+) -> str:
+    """Calculate the base object ID for an entity that will be set via set_object_id().
+
+    This function calculates what object_id_c_str_ should be set to in C++.
+
+    The C++ EntityBase::get_object_id() (entity_base.cpp lines 38-49) works as:
+    - If !has_own_name && is_name_add_mac_suffix_enabled():
+        return str_sanitize(str_snake_case(App.get_friendly_name()))  // Dynamic
+    - Else:
+        return object_id_c_str_ ?? ""  // What we set via set_object_id()
+
+    Since we're calculating what to pass to set_object_id(), we always need to
+    generate the object_id the same way, regardless of name_add_mac_suffix setting.
+
+    Args:
+        name: The entity name (empty string if no name)
+        friendly_name: The friendly name from CORE.friendly_name
+        device_name: The device name if entity is on a sub-device
+
+    Returns:
+        The base object ID to use for duplicate checking and to pass to set_object_id()
+    """
+
+    if name:
+        # Entity has its own name (has_own_name will be true)
+        base_str = name
+    elif device_name:
+        # Entity has empty name and is on a sub-device
+        # C++ EntityBase::set_name() uses device->get_name() when device is set
+        base_str = device_name
+    elif friendly_name:
+        # Entity has empty name (has_own_name will be false)
+        # C++ uses App.get_friendly_name() which returns friendly_name or device name
+        base_str = friendly_name
+    else:
+        # Fallback to device name
+        base_str = CORE.name
+
+    return sanitize(snake_case(base_str))
+
+
+async def setup_entity(var: MockObj, config: ConfigType, platform: str) -> None:
+    """Set up generic properties of an Entity.
+
+    This function sets up the common entity properties like name, icon,
+    entity category, etc.
+
+    Args:
+        var: The entity variable to set up
+        config: Configuration dictionary containing entity settings
+        platform: The platform name (e.g., "sensor", "binary_sensor")
+    """
+    # Get device info
+    device_name: str | None = None
+    if CONF_DEVICE_ID in config:
+        device_id_obj: ID = config[CONF_DEVICE_ID]
+        device: MockObj = await get_variable(device_id_obj)
+        add(var.set_device(device))
+        # Get device name for object ID calculation
+        device_name = device_id_obj.id
+
+    add(var.set_name(config[CONF_NAME]))
+
+    # Calculate base object_id using the same logic as C++
+    # This must match the C++ behavior in esphome/core/entity_base.cpp
+    base_object_id = get_base_entity_object_id(
+        config[CONF_NAME], CORE.friendly_name, device_name
+    )
+
+    if not config[CONF_NAME]:
+        _LOGGER.debug(
+            "Entity has empty name, using '%s' as object_id base", base_object_id
+        )
+
+    # Set the object ID
+    add(var.set_object_id(base_object_id))
+    _LOGGER.debug(
+        "Setting object_id '%s' for entity '%s' on platform '%s'",
+        base_object_id,
+        config[CONF_NAME],
+        platform,
+    )
+    add(var.set_disabled_by_default(config[CONF_DISABLED_BY_DEFAULT]))
+    if CONF_INTERNAL in config:
+        add(var.set_internal(config[CONF_INTERNAL]))
+    if CONF_ICON in config:
+        # Add USE_ENTITY_ICON define when icons are used
+        cg.add_define("USE_ENTITY_ICON")
+        add(var.set_icon(config[CONF_ICON]))
+    if CONF_ENTITY_CATEGORY in config:
+        add(var.set_entity_category(config[CONF_ENTITY_CATEGORY]))
 
 
 def inherit_property_from(property_to_inherit, parent_id_property, transform=None):
@@ -54,3 +168,50 @@ def inherit_property_from(property_to_inherit, parent_id_property, transform=Non
         return config
 
     return inherit_property
+
+
+def entity_duplicate_validator(platform: str) -> Callable[[ConfigType], ConfigType]:
+    """Create a validator function to check for duplicate entity names.
+
+    This validator is meant to be used with schema.add_extra() for entity base schemas.
+
+    Args:
+        platform: The platform name (e.g., "sensor", "binary_sensor")
+
+    Returns:
+        A validator function that checks for duplicate names
+    """
+
+    def validator(config: ConfigType) -> ConfigType:
+        if CONF_NAME not in config:
+            # No name to validate
+            return config
+
+        # Get the entity name
+        entity_name = config[CONF_NAME]
+
+        # Get device name if entity is on a sub-device
+        device_name = None
+        if CONF_DEVICE_ID in config:
+            device_id_obj = config[CONF_DEVICE_ID]
+            device_name = device_id_obj.id
+
+        # Calculate what object_id will actually be used
+        # This handles empty names correctly by using device/friendly names
+        name_key = get_base_entity_object_id(
+            entity_name, CORE.friendly_name, device_name
+        )
+
+        # Check for duplicates
+        unique_key = (platform, name_key)
+        if unique_key in CORE.unique_ids:
+            raise cv.Invalid(
+                f"Duplicate {platform} entity with name '{entity_name}' found. "
+                f"Each entity must have a unique name within its platform across all devices."
+            )
+
+        # Add to tracking set
+        CORE.unique_ids.add(unique_key)
+        return config
+
+    return validator
diff --git a/esphome/core/event_pool.h b/esphome/core/event_pool.h
new file mode 100644
index 0000000000..69e03bafac
--- /dev/null
+++ b/esphome/core/event_pool.h
@@ -0,0 +1,81 @@
+#pragma once
+
+#if defined(USE_ESP32) || defined(USE_LIBRETINY)
+
+#include 
+#include 
+#include "esphome/core/helpers.h"
+#include "esphome/core/lock_free_queue.h"
+
+namespace esphome {
+
+// Event Pool - On-demand pool of objects to avoid heap fragmentation
+// Events are allocated on first use and reused thereafter, growing to peak usage
+// @tparam T The type of objects managed by the pool (must have a release() method)
+// @tparam SIZE The maximum number of objects in the pool (1-255, limited by uint8_t)
+template class EventPool {
+ public:
+  EventPool() : total_created_(0) {}
+
+  ~EventPool() {
+    // Clean up any remaining events in the free list
+    // IMPORTANT: This destructor assumes no concurrent access. The EventPool must not
+    // be destroyed while any thread might still call allocate() or release().
+    // In practice, this is typically ensured by destroying the pool only during
+    // component shutdown when all producer/consumer threads have been stopped.
+    T *event;
+    RAMAllocator allocator(RAMAllocator::ALLOC_INTERNAL);
+    while ((event = this->free_list_.pop()) != nullptr) {
+      // Call destructor
+      event->~T();
+      // Deallocate using RAMAllocator
+      allocator.deallocate(event, 1);
+    }
+  }
+
+  // Allocate an event from the pool
+  // Returns nullptr if pool is full
+  T *allocate() {
+    // Try to get from free list first
+    T *event = this->free_list_.pop();
+    if (event != nullptr)
+      return event;
+
+    // Need to create a new event
+    if (this->total_created_ >= SIZE) {
+      // Pool is at capacity
+      return nullptr;
+    }
+
+    // Use internal RAM for better performance
+    RAMAllocator allocator(RAMAllocator::ALLOC_INTERNAL);
+    event = allocator.allocate(1);
+
+    if (event == nullptr) {
+      // Memory allocation failed
+      return nullptr;
+    }
+
+    // Placement new to construct the object
+    new (event) T();
+    this->total_created_++;
+    return event;
+  }
+
+  // Return an event to the pool for reuse
+  void release(T *event) {
+    if (event != nullptr) {
+      // Clean up the event's allocated memory
+      event->release();
+      this->free_list_.push(event);
+    }
+  }
+
+ private:
+  LockFreeQueue free_list_;  // Free events ready for reuse
+  uint8_t total_created_;             // Total events created (high water mark, max 255)
+};
+
+}  // namespace esphome
+
+#endif  // defined(USE_ESP32) || defined(USE_LIBRETINY)
diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp
index ec79cb8bbb..b46077af02 100644
--- a/esphome/core/helpers.cpp
+++ b/esphome/core/helpers.cpp
@@ -4,53 +4,16 @@
 #include "esphome/core/hal.h"
 #include "esphome/core/log.h"
 
+#include 
 #include 
 #include 
 #include 
 #include 
 #include 
 #include 
-#include 
 
-#ifdef USE_HOST
-#ifndef _WIN32
-#include 
-#include 
-#include 
-#endif
-#include 
-#endif
-#if defined(USE_ESP8266)
-#include 
-#include 
-// for xt_rsil()/xt_wsr_ps()
-#include 
-#elif defined(USE_ESP32_FRAMEWORK_ARDUINO)
-#include 
-#elif defined(USE_ESP_IDF)
-#include 
-#include 
-#include "esp_random.h"
-#include "esp_system.h"
-#elif defined(USE_RP2040)
-#if defined(USE_WIFI)
-#include 
-#endif
-#include 
-#include 
-#elif defined(USE_HOST)
-#include 
-#include 
-#endif
 #ifdef USE_ESP32
 #include "rom/crc.h"
-#include "esp_mac.h"
-#include "esp_efuse.h"
-#include "esp_efuse_table.h"
-#endif
-
-#ifdef USE_LIBRETINY
-#include   // for macAddress()
 #endif
 
 namespace esphome {
@@ -76,23 +39,8 @@ static const uint16_t CRC16_1021_BE_LUT_H[] = {0x0000, 0x1231, 0x2462, 0x3653, 0
                                                0x9188, 0x83b9, 0xb5ea, 0xa7db, 0xd94c, 0xcb7d, 0xfd2e, 0xef1f};
 #endif
 
-// STL backports
-
-#if _GLIBCXX_RELEASE < 8
-std::string to_string(int value) { return str_snprintf("%d", 32, value); }                   // NOLINT
-std::string to_string(long value) { return str_snprintf("%ld", 32, value); }                 // NOLINT
-std::string to_string(long long value) { return str_snprintf("%lld", 32, value); }           // NOLINT
-std::string to_string(unsigned value) { return str_snprintf("%u", 32, value); }              // NOLINT
-std::string to_string(unsigned long value) { return str_snprintf("%lu", 32, value); }        // NOLINT
-std::string to_string(unsigned long long value) { return str_snprintf("%llu", 32, value); }  // NOLINT
-std::string to_string(float value) { return str_snprintf("%f", 32, value); }
-std::string to_string(double value) { return str_snprintf("%f", 32, value); }
-std::string to_string(long double value) { return str_snprintf("%Lf", 32, value); }
-#endif
-
 // Mathematics
 
-float lerp(float completion, float start, float end) { return start + (end - start) * completion; }
 uint8_t crc8(const uint8_t *data, uint8_t len) {
   uint8_t crc = 0;
 
@@ -192,70 +140,7 @@ uint32_t fnv1_hash(const std::string &str) {
   return hash;
 }
 
-#ifdef USE_ESP32
-uint32_t random_uint32() { return esp_random(); }
-#elif defined(USE_ESP8266)
-uint32_t random_uint32() { return os_random(); }
-#elif defined(USE_RP2040)
-uint32_t random_uint32() {
-  uint32_t result = 0;
-  for (uint8_t i = 0; i < 32; i++) {
-    result <<= 1;
-    result |= rosc_hw->randombit;
-  }
-  return result;
-}
-#elif defined(USE_LIBRETINY)
-uint32_t random_uint32() { return rand(); }
-#elif defined(USE_HOST)
-uint32_t random_uint32() {
-  std::random_device dev;
-  std::mt19937 rng(dev());
-  std::uniform_int_distribution dist(0, std::numeric_limits::max());
-  return dist(rng);
-}
-#endif
 float random_float() { return static_cast(random_uint32()) / static_cast(UINT32_MAX); }
-#ifdef USE_ESP32
-bool random_bytes(uint8_t *data, size_t len) {
-  esp_fill_random(data, len);
-  return true;
-}
-#elif defined(USE_ESP8266)
-bool random_bytes(uint8_t *data, size_t len) { return os_get_random(data, len) == 0; }
-#elif defined(USE_RP2040)
-bool random_bytes(uint8_t *data, size_t len) {
-  while (len-- != 0) {
-    uint8_t result = 0;
-    for (uint8_t i = 0; i < 8; i++) {
-      result <<= 1;
-      result |= rosc_hw->randombit;
-    }
-    *data++ = result;
-  }
-  return true;
-}
-#elif defined(USE_LIBRETINY)
-bool random_bytes(uint8_t *data, size_t len) {
-  lt_rand_bytes(data, len);
-  return true;
-}
-#elif defined(USE_HOST)
-bool random_bytes(uint8_t *data, size_t len) {
-  FILE *fp = fopen("/dev/urandom", "r");
-  if (fp == nullptr) {
-    ESP_LOGW(TAG, "Could not open /dev/urandom, errno=%d", errno);
-    exit(1);
-  }
-  size_t read = fread(data, 1, len, fp);
-  if (read != len) {
-    ESP_LOGW(TAG, "Not enough data from /dev/urandom");
-    exit(1);
-  }
-  fclose(fp);
-  return true;
-}
-#endif
 
 // Strings
 
@@ -356,6 +241,10 @@ size_t parse_hex(const char *str, size_t length, uint8_t *data, size_t count) {
   return chars;
 }
 
+std::string format_mac_address_pretty(const uint8_t *mac) {
+  return str_snprintf("%02X:%02X:%02X:%02X:%02X:%02X", 17, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
+}
+
 static char format_hex_char(uint8_t v) { return v >= 10 ? 'a' + (v - 10) : '0' + v; }
 std::string format_hex(const uint8_t *data, size_t length) {
   std::string ret;
@@ -369,41 +258,63 @@ std::string format_hex(const uint8_t *data, size_t length) {
 std::string format_hex(const std::vector &data) { return format_hex(data.data(), data.size()); }
 
 static char format_hex_pretty_char(uint8_t v) { return v >= 10 ? 'A' + (v - 10) : '0' + v; }
-std::string format_hex_pretty(const uint8_t *data, size_t length) {
-  if (length == 0)
+std::string format_hex_pretty(const uint8_t *data, size_t length, char separator, bool show_length) {
+  if (data == nullptr || length == 0)
     return "";
   std::string ret;
-  ret.resize(3 * length - 1);
+  uint8_t multiple = separator ? 3 : 2;  // 3 if separator is not \0, 2 otherwise
+  ret.resize(multiple * length - (separator ? 1 : 0));
   for (size_t i = 0; i < length; i++) {
-    ret[3 * i] = format_hex_pretty_char((data[i] & 0xF0) >> 4);
-    ret[3 * i + 1] = format_hex_pretty_char(data[i] & 0x0F);
-    if (i != length - 1)
-      ret[3 * i + 2] = '.';
+    ret[multiple * i] = format_hex_pretty_char((data[i] & 0xF0) >> 4);
+    ret[multiple * i + 1] = format_hex_pretty_char(data[i] & 0x0F);
+    if (separator && i != length - 1)
+      ret[multiple * i + 2] = separator;
   }
-  if (length > 4)
-    return ret + " (" + to_string(length) + ")";
+  if (show_length && length > 4)
+    return ret + " (" + std::to_string(length) + ")";
   return ret;
 }
-std::string format_hex_pretty(const std::vector &data) { return format_hex_pretty(data.data(), data.size()); }
+std::string format_hex_pretty(const std::vector &data, char separator, bool show_length) {
+  return format_hex_pretty(data.data(), data.size(), separator, show_length);
+}
 
-std::string format_hex_pretty(const uint16_t *data, size_t length) {
-  if (length == 0)
+std::string format_hex_pretty(const uint16_t *data, size_t length, char separator, bool show_length) {
+  if (data == nullptr || length == 0)
     return "";
   std::string ret;
-  ret.resize(5 * length - 1);
+  uint8_t multiple = separator ? 5 : 4;  // 5 if separator is not \0, 4 otherwise
+  ret.resize(multiple * length - (separator ? 1 : 0));
   for (size_t i = 0; i < length; i++) {
-    ret[5 * i] = format_hex_pretty_char((data[i] & 0xF000) >> 12);
-    ret[5 * i + 1] = format_hex_pretty_char((data[i] & 0x0F00) >> 8);
-    ret[5 * i + 2] = format_hex_pretty_char((data[i] & 0x00F0) >> 4);
-    ret[5 * i + 3] = format_hex_pretty_char(data[i] & 0x000F);
-    if (i != length - 1)
-      ret[5 * i + 2] = '.';
+    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 + 2] = format_hex_pretty_char((data[i] & 0x00F0) >> 4);
+    ret[multiple * i + 3] = format_hex_pretty_char(data[i] & 0x000F);
+    if (separator && i != length - 1)
+      ret[multiple * i + 4] = separator;
   }
-  if (length > 4)
-    return ret + " (" + to_string(length) + ")";
+  if (show_length && length > 4)
+    return ret + " (" + std::to_string(length) + ")";
+  return ret;
+}
+std::string format_hex_pretty(const std::vector &data, char separator, bool show_length) {
+  return format_hex_pretty(data.data(), data.size(), separator, show_length);
+}
+std::string format_hex_pretty(const std::string &data, char separator, bool show_length) {
+  if (data.empty())
+    return "";
+  std::string ret;
+  uint8_t multiple = separator ? 3 : 2;  // 3 if separator is not \0, 2 otherwise
+  ret.resize(multiple * data.length() - (separator ? 1 : 0));
+  for (size_t i = 0; i < data.length(); i++) {
+    ret[multiple * i] = format_hex_pretty_char((data[i] & 0xF0) >> 4);
+    ret[multiple * i + 1] = format_hex_pretty_char(data[i] & 0x0F);
+    if (separator && i != data.length() - 1)
+      ret[multiple * i + 2] = separator;
+  }
+  if (show_length && data.length() > 4)
+    return ret + " (" + std::to_string(data.length()) + ")";
   return ret;
 }
-std::string format_hex_pretty(const std::vector &data) { return format_hex_pretty(data.data(), data.size()); }
 
 std::string format_bin(const uint8_t *data, size_t length) {
   std::string result;
@@ -456,9 +367,22 @@ int8_t step_to_accuracy_decimals(float step) {
   return str.length() - dot_pos - 1;
 }
 
-static const std::string BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
-                                        "abcdefghijklmnopqrstuvwxyz"
-                                        "0123456789+/";
+// Use C-style string constant to store in ROM instead of RAM (saves 24 bytes)
+static constexpr const char *BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+                                            "abcdefghijklmnopqrstuvwxyz"
+                                            "0123456789+/";
+
+// Helper function to find the index of a base64 character in the lookup table.
+// Returns the character's position (0-63) if found, or 0 if not found.
+// NOTE: This returns 0 for both 'A' (valid base64 char at index 0) and invalid characters.
+// This is safe because is_base64() is ALWAYS checked before calling this function,
+// preventing invalid characters from ever reaching here. The base64_decode function
+// stops processing at the first invalid character due to the is_base64() check in its
+// while loop condition, making this edge case harmless in practice.
+static inline uint8_t base64_find_char(char c) {
+  const char *pos = strchr(BASE64_CHARS, c);
+  return pos ? (pos - BASE64_CHARS) : 0;
+}
 
 static inline bool is_base64(char c) { return (isalnum(c) || (c == '+') || (c == '/')); }
 
@@ -480,7 +404,7 @@ std::string base64_encode(const uint8_t *buf, size_t buf_len) {
       char_array_4[3] = char_array_3[2] & 0x3f;
 
       for (i = 0; (i < 4); i++)
-        ret += BASE64_CHARS[char_array_4[i]];
+        ret += BASE64_CHARS[static_cast(char_array_4[i])];
       i = 0;
     }
   }
@@ -495,7 +419,7 @@ std::string base64_encode(const uint8_t *buf, size_t buf_len) {
     char_array_4[3] = char_array_3[2] & 0x3f;
 
     for (j = 0; (j < i + 1); j++)
-      ret += BASE64_CHARS[char_array_4[j]];
+      ret += BASE64_CHARS[static_cast(char_array_4[j])];
 
     while ((i++ < 3))
       ret += '=';
@@ -522,12 +446,15 @@ std::vector base64_decode(const std::string &encoded_string) {
   uint8_t char_array_4[4], char_array_3[3];
   std::vector ret;
 
+  // SAFETY: The loop condition checks is_base64() before processing each character.
+  // This ensures base64_find_char() is only called on valid base64 characters,
+  // preventing the edge case where invalid chars would return 0 (same as 'A').
   while (in_len-- && (encoded_string[in] != '=') && is_base64(encoded_string[in])) {
     char_array_4[i++] = encoded_string[in];
     in++;
     if (i == 4) {
       for (i = 0; i < 4; i++)
-        char_array_4[i] = BASE64_CHARS.find(char_array_4[i]);
+        char_array_4[i] = base64_find_char(char_array_4[i]);
 
       char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
       char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
@@ -544,7 +471,7 @@ std::vector base64_decode(const std::string &encoded_string) {
       char_array_4[j] = 0;
 
     for (j = 0; j < 4; j++)
-      char_array_4[j] = BASE64_CHARS.find(char_array_4[j]);
+      char_array_4[j] = base64_find_char(char_array_4[j]);
 
     char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
     char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
@@ -640,35 +567,6 @@ void hsv_to_rgb(int hue, float saturation, float value, float &red, float &green
   blue += delta;
 }
 
-// System APIs
-#if defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_HOST)
-// ESP8266 doesn't have mutexes, but that shouldn't be an issue as it's single-core and non-preemptive OS.
-Mutex::Mutex() {}
-Mutex::~Mutex() {}
-void Mutex::lock() {}
-bool Mutex::try_lock() { return true; }
-void Mutex::unlock() {}
-#elif defined(USE_ESP32) || defined(USE_LIBRETINY)
-Mutex::Mutex() { handle_ = xSemaphoreCreateMutex(); }
-Mutex::~Mutex() {}
-void Mutex::lock() { xSemaphoreTake(this->handle_, portMAX_DELAY); }
-bool Mutex::try_lock() { return xSemaphoreTake(this->handle_, 0) == pdTRUE; }
-void Mutex::unlock() { xSemaphoreGive(this->handle_); }
-#endif
-
-#if defined(USE_ESP8266)
-IRAM_ATTR InterruptLock::InterruptLock() { state_ = xt_rsil(15); }
-IRAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(state_); }
-#elif defined(USE_ESP32) || defined(USE_LIBRETINY)
-// only affects the executing core
-// so should not be used as a mutex lock, only to get accurate timing
-IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); }
-IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); }
-#elif defined(USE_RP2040)
-IRAM_ATTR InterruptLock::InterruptLock() { state_ = save_and_disable_interrupts(); }
-IRAM_ATTR InterruptLock::~InterruptLock() { restore_interrupts(state_); }
-#endif
-
 uint8_t HighFrequencyLoopRequester::num_requests = 0;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
 void HighFrequencyLoopRequester::start() {
   if (this->started_)
@@ -684,45 +582,6 @@ void HighFrequencyLoopRequester::stop() {
 }
 bool HighFrequencyLoopRequester::is_high_frequency() { return num_requests > 0; }
 
-#if defined(USE_HOST)
-void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parameter)
-  static const uint8_t esphome_host_mac_address[6] = USE_ESPHOME_HOST_MAC_ADDRESS;
-  memcpy(mac, esphome_host_mac_address, sizeof(esphome_host_mac_address));
-}
-#elif defined(USE_ESP32)
-void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parameter)
-#if defined(CONFIG_SOC_IEEE802154_SUPPORTED)
-  // When CONFIG_SOC_IEEE802154_SUPPORTED is defined, esp_efuse_mac_get_default
-  // returns the 802.15.4 EUI-64 address, so we read directly from eFuse instead.
-  if (has_custom_mac_address()) {
-    esp_efuse_read_field_blob(ESP_EFUSE_MAC_CUSTOM, mac, 48);
-  } else {
-    esp_efuse_read_field_blob(ESP_EFUSE_MAC_FACTORY, mac, 48);
-  }
-#else
-  if (has_custom_mac_address()) {
-    esp_efuse_mac_get_custom(mac);
-  } else {
-    esp_efuse_mac_get_default(mac);
-  }
-#endif
-}
-#elif defined(USE_ESP8266)
-void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parameter)
-  wifi_get_macaddr(STATION_IF, mac);
-}
-#elif defined(USE_RP2040)
-void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parameter)
-#ifdef USE_WIFI
-  WiFi.macAddress(mac);
-#endif
-}
-#elif defined(USE_LIBRETINY)
-void get_mac_address_raw(uint8_t *mac) {  // NOLINT(readability-non-const-parameter)
-  WiFi.macAddress(mac);
-}
-#endif
-
 std::string get_mac_address() {
   uint8_t mac[6];
   get_mac_address_raw(mac);
@@ -732,27 +591,13 @@ std::string get_mac_address() {
 std::string get_mac_address_pretty() {
   uint8_t mac[6];
   get_mac_address_raw(mac);
-  return str_snprintf("%02X:%02X:%02X:%02X:%02X:%02X", 17, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
+  return format_mac_address_pretty(mac);
 }
 
-#ifdef USE_ESP32
-void set_mac_address(uint8_t *mac) { esp_base_mac_addr_set(mac); }
+#ifndef USE_ESP32
+bool has_custom_mac_address() { return false; }
 #endif
 
-bool has_custom_mac_address() {
-#if defined(USE_ESP32) && !defined(USE_ESP32_IGNORE_EFUSE_CUSTOM_MAC)
-  uint8_t mac[6];
-  // do not use 'esp_efuse_mac_get_custom(mac)' because it drops an error in the logs whenever it fails
-#ifndef USE_ESP32_VARIANT_ESP32
-  return (esp_efuse_read_field_blob(ESP_EFUSE_USER_DATA_MAC_CUSTOM, mac, 48) == ESP_OK) && mac_address_is_valid(mac);
-#else
-  return (esp_efuse_read_field_blob(ESP_EFUSE_MAC_CUSTOM, mac, 48) == ESP_OK) && mac_address_is_valid(mac);
-#endif
-#else
-  return false;
-#endif
-}
-
 bool mac_address_is_valid(const uint8_t *mac) {
   bool is_all_zeros = true;
   bool is_all_ones = true;
diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h
index 477f260bf0..58f162ff9d 100644
--- a/esphome/core/helpers.h
+++ b/esphome/core/helpers.h
@@ -32,94 +32,27 @@
 #include 
 #endif
 
+#ifdef USE_HOST
+#include 
+#endif
+
 #define HOT __attribute__((hot))
 #define ESPDEPRECATED(msg, when) __attribute__((deprecated(msg)))
 #define ESPHOME_ALWAYS_INLINE __attribute__((always_inline))
 #define PACKED __attribute__((packed))
 
-// Various functions can be constexpr in C++14, but not in C++11 (because their body isn't just a return statement).
-// Define a substitute constexpr keyword for those functions, until we can drop C++11 support.
-#if __cplusplus >= 201402L
-#define constexpr14 constexpr
-#else
-#define constexpr14 inline  // constexpr implies inline
-#endif
-
 namespace esphome {
 
 /// @name STL backports
 ///@{
 
-// Backports for various STL features we like to use. Pull in the STL implementation wherever available, to avoid
-// ambiguity and to provide a uniform API.
-
-// std::to_string() from C++11, available from libstdc++/g++ 8
-// See https://github.com/espressif/esp-idf/issues/1445
-#if _GLIBCXX_RELEASE >= 8
+// Keep "using" even after the removal of our backports, to avoid breaking existing code.
 using std::to_string;
-#else
-std::string to_string(int value);                 // NOLINT
-std::string to_string(long value);                // NOLINT
-std::string to_string(long long value);           // NOLINT
-std::string to_string(unsigned value);            // NOLINT
-std::string to_string(unsigned long value);       // NOLINT
-std::string to_string(unsigned long long value);  // NOLINT
-std::string to_string(float value);
-std::string to_string(double value);
-std::string to_string(long double value);
-#endif
-
-// std::is_trivially_copyable from C++11, implemented in libstdc++/g++ 5.1 (but minor releases can't be detected)
-#if _GLIBCXX_RELEASE >= 6
 using std::is_trivially_copyable;
-#else
-// Implementing this is impossible without compiler intrinsics, so don't bother. Invalid usage will be detected on
-// other variants that use a newer compiler anyway.
-// NOLINTNEXTLINE(readability-identifier-naming)
-template struct is_trivially_copyable : public std::integral_constant {};
-#endif
-
-// std::make_unique() from C++14
-#if __cpp_lib_make_unique >= 201304
 using std::make_unique;
-#else
-template std::unique_ptr make_unique(Args &&...args) {
-  return std::unique_ptr(new T(std::forward(args)...));
-}
-#endif
-
-// std::enable_if_t from C++14
-#if __cplusplus >= 201402L
 using std::enable_if_t;
-#else
-template using enable_if_t = typename std::enable_if::type;
-#endif
-
-// std::clamp from C++17
-#if __cpp_lib_clamp >= 201603
 using std::clamp;
-#else
-template constexpr const T &clamp(const T &v, const T &lo, const T &hi, Compare comp) {
-  return comp(v, lo) ? lo : comp(hi, v) ? hi : v;
-}
-template constexpr const T &clamp(const T &v, const T &lo, const T &hi) {
-  return clamp(v, lo, hi, std::less{});
-}
-#endif
-
-// std::is_invocable from C++17
-#if __cpp_lib_is_invocable >= 201703
 using std::is_invocable;
-#else
-// https://stackoverflow.com/a/37161919/8924614
-template struct is_invocable {  // NOLINT(readability-identifier-naming)
-  template static auto test(U *p) -> decltype((*p)(std::declval()...), void(), std::true_type());
-  template static auto test(...) -> decltype(std::false_type());
-  static constexpr auto value = decltype(test(nullptr))::value;  // NOLINT
-};
-#endif
-
-// std::bit_cast from C++20
 #if __cpp_lib_bit_cast >= 201806
 using std::bit_cast;
 #else
@@ -134,31 +67,29 @@ To bit_cast(const From &src) {
   return dst;
 }
 #endif
+using std::lerp;
 
 // std::byteswap from C++23
-template constexpr14 T byteswap(T n) {
+template constexpr T byteswap(T n) {
   T m;
   for (size_t i = 0; i < sizeof(T); i++)
     reinterpret_cast(&m)[i] = reinterpret_cast(&n)[sizeof(T) - 1 - i];
   return m;
 }
-template<> constexpr14 uint8_t byteswap(uint8_t n) { return n; }
-template<> constexpr14 uint16_t byteswap(uint16_t n) { return __builtin_bswap16(n); }
-template<> constexpr14 uint32_t byteswap(uint32_t n) { return __builtin_bswap32(n); }
-template<> constexpr14 uint64_t byteswap(uint64_t n) { return __builtin_bswap64(n); }
-template<> constexpr14 int8_t byteswap(int8_t n) { return n; }
-template<> constexpr14 int16_t byteswap(int16_t n) { return __builtin_bswap16(n); }
-template<> constexpr14 int32_t byteswap(int32_t n) { return __builtin_bswap32(n); }
-template<> constexpr14 int64_t byteswap(int64_t n) { return __builtin_bswap64(n); }
+template<> constexpr uint8_t byteswap(uint8_t n) { return n; }
+template<> constexpr uint16_t byteswap(uint16_t n) { return __builtin_bswap16(n); }
+template<> constexpr uint32_t byteswap(uint32_t n) { return __builtin_bswap32(n); }
+template<> constexpr uint64_t byteswap(uint64_t n) { return __builtin_bswap64(n); }
+template<> constexpr int8_t byteswap(int8_t n) { return n; }
+template<> constexpr int16_t byteswap(int16_t n) { return __builtin_bswap16(n); }
+template<> constexpr int32_t byteswap(int32_t n) { return __builtin_bswap32(n); }
+template<> constexpr int64_t byteswap(int64_t n) { return __builtin_bswap64(n); }
 
 ///@}
 
 /// @name Mathematics
 ///@{
 
-/// Linearly interpolate between \p start and \p end by \p completion (between 0 and 1).
-float lerp(float completion, float start, float end);
-
 /// Remap \p value from the range (\p min, \p max) to (\p min_out, \p max_out).
 template T remap(U value, U min, U max, T min_out, T max_out) {
   return (value - min) * (max_out - min_out) / (max - min) + min_out;
@@ -203,8 +134,7 @@ constexpr uint32_t encode_uint32(uint8_t byte1, uint8_t byte2, uint8_t byte3, ui
 }
 
 /// Encode a value from its constituent bytes (from most to least significant) in an array with length sizeof(T).
-template::value, int> = 0>
-constexpr14 T encode_value(const uint8_t *bytes) {
+template::value, int> = 0> constexpr T encode_value(const uint8_t *bytes) {
   T val = 0;
   for (size_t i = 0; i < sizeof(T); i++) {
     val <<= 8;
@@ -214,12 +144,12 @@ constexpr14 T encode_value(const uint8_t *bytes) {
 }
 /// Encode a value from its constituent bytes (from most to least significant) in an std::array with length sizeof(T).
 template::value, int> = 0>
-constexpr14 T encode_value(const std::array bytes) {
+constexpr T encode_value(const std::array bytes) {
   return encode_value(bytes.data());
 }
 /// Decode a value into its constituent bytes (from most to least significant).
 template::value, int> = 0>
-constexpr14 std::array decode_value(T val) {
+constexpr std::array decode_value(T val) {
   std::array ret{};
   for (size_t i = sizeof(T); i > 0; i--) {
     ret[i - 1] = val & 0xFF;
@@ -246,7 +176,7 @@ inline uint32_t reverse_bits(uint32_t x) {
 }
 
 /// Convert a value between host byte order and big endian (most significant byte first) order.
-template constexpr14 T convert_big_endian(T val) {
+template constexpr T convert_big_endian(T val) {
 #if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
   return byteswap(val);
 #else
@@ -255,7 +185,7 @@ template constexpr14 T convert_big_endian(T val) {
 }
 
 /// Convert a value between host byte order and little endian (least significant byte first) order.
-template constexpr14 T convert_little_endian(T val) {
+template constexpr T convert_little_endian(T val) {
 #if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
   return val;
 #else
@@ -276,9 +206,6 @@ bool str_startswith(const std::string &str, const std::string &start);
 /// Check whether a string ends with a value.
 bool str_endswith(const std::string &str, const std::string &end);
 
-/// Convert the value to a string (added as extra overload so that to_string() can be used on all stringifiable types).
-inline std::string to_string(const std::string &val) { return val; }
-
 /// Truncate a string to a specific length.
 std::string str_truncate(const std::string &str, size_t length);
 
@@ -402,6 +329,8 @@ template::value, int> = 0> optional<
   return parse_hex(str.c_str(), str.length());
 }
 
+/// Format the six-byte array \p mac into a MAC address.
+std::string format_mac_address_pretty(const uint8_t mac[6]);
 /// Format the byte array \p data of length \p len in lowercased hex.
 std::string format_hex(const uint8_t *data, size_t length);
 /// Format the vector \p data in lowercased hex.
@@ -415,18 +344,149 @@ template std::string format_hex(const std::array &dat
   return format_hex(data.data(), data.size());
 }
 
-/// Format the byte array \p data of length \p len in pretty-printed, human-readable hex.
-std::string format_hex_pretty(const uint8_t *data, size_t length);
-/// Format the word array \p data of length \p len in pretty-printed, human-readable hex.
-std::string format_hex_pretty(const uint16_t *data, size_t length);
-/// Format the vector \p data in pretty-printed, human-readable hex.
-std::string format_hex_pretty(const std::vector &data);
-/// Format the vector \p data in pretty-printed, human-readable hex.
-std::string format_hex_pretty(const std::vector &data);
-/// Format an unsigned integer in pretty-printed, human-readable hex, starting with the most significant byte.
-template::value, int> = 0> std::string format_hex_pretty(T val) {
+/** Format a byte array in pretty-printed, human-readable hex format.
+ *
+ * Converts binary data to a hexadecimal string representation with customizable formatting.
+ * Each byte is displayed as a two-digit uppercase hex value, separated by the specified separator.
+ * Optionally includes the total byte count in parentheses at the end.
+ *
+ * @param data Pointer to the byte array to format.
+ * @param length Number of bytes in the array.
+ * @param separator Character to use between hex bytes (default: '.').
+ * @param show_length Whether to append the byte count in parentheses (default: true).
+ * @return Formatted hex string, e.g., "A1.B2.C3.D4.E5 (5)" or "A1:B2:C3" depending on parameters.
+ *
+ * @note Returns empty string if data is nullptr or length is 0.
+ * @note The length will only be appended if show_length is true AND the length is greater than 4.
+ *
+ * Example:
+ * @code
+ * uint8_t data[] = {0xA1, 0xB2, 0xC3};
+ * format_hex_pretty(data, 3);           // Returns "A1.B2.C3" (no length shown for <= 4 parts)
+ * uint8_t data2[] = {0xA1, 0xB2, 0xC3, 0xD4, 0xE5};
+ * format_hex_pretty(data2, 5);          // Returns "A1.B2.C3.D4.E5 (5)"
+ * format_hex_pretty(data2, 5, ':');     // Returns "A1:B2:C3:D4:E5 (5)"
+ * format_hex_pretty(data2, 5, '.', false); // Returns "A1.B2.C3.D4.E5"
+ * @endcode
+ */
+std::string format_hex_pretty(const uint8_t *data, size_t length, char separator = '.', bool show_length = true);
+
+/** Format a 16-bit word array in pretty-printed, human-readable hex format.
+ *
+ * Similar to the byte array version, but formats 16-bit words as 4-digit hex values.
+ *
+ * @param data Pointer to the 16-bit word array to format.
+ * @param length Number of 16-bit words in the array.
+ * @param separator Character to use between hex words (default: '.').
+ * @param show_length Whether to append the word count in parentheses (default: true).
+ * @return Formatted hex string with 4-digit hex values per word.
+ *
+ * @note The length will only be appended if show_length is true AND the length is greater than 4.
+ *
+ * Example:
+ * @code
+ * uint16_t data[] = {0xA1B2, 0xC3D4};
+ * format_hex_pretty(data, 2); // Returns "A1B2.C3D4" (no length shown for <= 4 parts)
+ * uint16_t data2[] = {0xA1B2, 0xC3D4, 0xE5F6};
+ * format_hex_pretty(data2, 3); // Returns "A1B2.C3D4.E5F6 (3)"
+ * @endcode
+ */
+std::string format_hex_pretty(const uint16_t *data, size_t length, char separator = '.', bool show_length = true);
+
+/** Format a byte vector in pretty-printed, human-readable hex format.
+ *
+ * Convenience overload for std::vector. Formats each byte as a two-digit
+ * uppercase hex value with customizable separator.
+ *
+ * @param data Vector of bytes to format.
+ * @param separator Character to use between hex bytes (default: '.').
+ * @param show_length Whether to append the byte count in parentheses (default: true).
+ * @return Formatted hex string representation of the vector contents.
+ *
+ * @note The length will only be appended if show_length is true AND the vector size is greater than 4.
+ *
+ * Example:
+ * @code
+ * std::vector data = {0xDE, 0xAD, 0xBE, 0xEF};
+ * format_hex_pretty(data);        // Returns "DE.AD.BE.EF" (no length shown for <= 4 parts)
+ * std::vector data2 = {0xDE, 0xAD, 0xBE, 0xEF, 0xCA};
+ * format_hex_pretty(data2);       // Returns "DE.AD.BE.EF.CA (5)"
+ * format_hex_pretty(data2, '-');  // Returns "DE-AD-BE-EF-CA (5)"
+ * @endcode
+ */
+std::string format_hex_pretty(const std::vector &data, char separator = '.', bool show_length = true);
+
+/** Format a 16-bit word vector in pretty-printed, human-readable hex format.
+ *
+ * Convenience overload for std::vector. Each 16-bit word is formatted
+ * as a 4-digit uppercase hex value in big-endian order.
+ *
+ * @param data Vector of 16-bit words to format.
+ * @param separator Character to use between hex words (default: '.').
+ * @param show_length Whether to append the word count in parentheses (default: true).
+ * @return Formatted hex string representation of the vector contents.
+ *
+ * @note The length will only be appended if show_length is true AND the vector size is greater than 4.
+ *
+ * Example:
+ * @code
+ * std::vector data = {0x1234, 0x5678};
+ * format_hex_pretty(data); // Returns "1234.5678" (no length shown for <= 4 parts)
+ * std::vector data2 = {0x1234, 0x5678, 0x9ABC};
+ * format_hex_pretty(data2); // Returns "1234.5678.9ABC (3)"
+ * @endcode
+ */
+std::string format_hex_pretty(const std::vector &data, char separator = '.', bool show_length = true);
+
+/** Format a string's bytes in pretty-printed, human-readable hex format.
+ *
+ * Treats each character in the string as a byte and formats it in hex.
+ * Useful for debugging binary data stored in std::string containers.
+ *
+ * @param data String whose bytes should be formatted as hex.
+ * @param separator Character to use between hex bytes (default: '.').
+ * @param show_length Whether to append the byte count in parentheses (default: true).
+ * @return Formatted hex string representation of the string's byte contents.
+ *
+ * @note The length will only be appended if show_length is true AND the string length is greater than 4.
+ *
+ * Example:
+ * @code
+ * std::string data = "ABC";  // ASCII: 0x41, 0x42, 0x43
+ * format_hex_pretty(data);   // Returns "41.42.43" (no length shown for <= 4 parts)
+ * std::string data2 = "ABCDE";
+ * format_hex_pretty(data2);  // Returns "41.42.43.44.45 (5)"
+ * @endcode
+ */
+std::string format_hex_pretty(const std::string &data, char separator = '.', bool show_length = true);
+
+/** Format an unsigned integer in pretty-printed, human-readable hex format.
+ *
+ * Converts the integer to big-endian byte order and formats each byte as hex.
+ * The most significant byte appears first in the output string.
+ *
+ * @tparam T Unsigned integer type (uint8_t, uint16_t, uint32_t, uint64_t, etc.).
+ * @param val The unsigned integer value to format.
+ * @param separator Character to use between hex bytes (default: '.').
+ * @param show_length Whether to append the byte count in parentheses (default: true).
+ * @return Formatted hex string with most significant byte first.
+ *
+ * @note The length will only be appended if show_length is true AND sizeof(T) is greater than 4.
+ *
+ * Example:
+ * @code
+ * uint32_t value = 0x12345678;
+ * format_hex_pretty(value);        // Returns "12.34.56.78" (no length shown for <= 4 parts)
+ * uint64_t value2 = 0x123456789ABCDEF0;
+ * format_hex_pretty(value2);       // Returns "12.34.56.78.9A.BC.DE.F0 (8)"
+ * format_hex_pretty(value2, ':');  // Returns "12:34:56:78:9A:BC:DE:F0 (8)"
+ * format_hex_pretty(0x1234); // Returns "12.34"
+ * @endcode
+ */
+template::value, int> = 0>
+std::string format_hex_pretty(T val, char separator = '.', bool show_length = true) {
   val = convert_big_endian(val);
-  return format_hex_pretty(reinterpret_cast(&val), sizeof(T));
+  return format_hex_pretty(reinterpret_cast(&val), sizeof(T), separator, show_length);
 }
 
 /// Format the byte array \p data of length \p len in binary.
diff --git a/esphome/core/lock_free_queue.h b/esphome/core/lock_free_queue.h
new file mode 100644
index 0000000000..f35cfa5af9
--- /dev/null
+++ b/esphome/core/lock_free_queue.h
@@ -0,0 +1,151 @@
+#pragma once
+
+#if defined(USE_ESP32) || defined(USE_LIBRETINY)
+
+#include 
+#include 
+
+#if defined(USE_ESP32)
+#include 
+#include 
+#elif defined(USE_LIBRETINY)
+#include 
+#include 
+#endif
+
+/*
+ * Lock-free queue for single-producer single-consumer scenarios.
+ * This allows one thread to push items and another to pop them without
+ * blocking each other.
+ *
+ * This is a Single-Producer Single-Consumer (SPSC) lock-free ring buffer.
+ * Available on platforms with FreeRTOS support (ESP32, LibreTiny).
+ *
+ * Common use cases:
+ * - BLE events: BLE task produces, main loop consumes
+ * - MQTT messages: main task produces, MQTT thread consumes
+ *
+ * @tparam T The type of elements stored in the queue (must be a pointer type)
+ * @tparam SIZE The maximum number of elements (1-255, limited by uint8_t indices)
+ */
+
+namespace esphome {
+
+// Base lock-free queue without task notification
+template class LockFreeQueue {
+ public:
+  LockFreeQueue() : head_(0), tail_(0), dropped_count_(0) {}
+
+  bool push(T *element) {
+    bool was_empty;
+    uint8_t old_tail;
+    return push_internal_(element, was_empty, old_tail);
+  }
+
+ protected:
+  // Internal push that reports queue state - for use by derived classes
+  bool push_internal_(T *element, bool &was_empty, uint8_t &old_tail) {
+    if (element == nullptr)
+      return false;
+
+    uint8_t current_tail = tail_.load(std::memory_order_relaxed);
+    uint8_t next_tail = (current_tail + 1) % SIZE;
+
+    // Read head before incrementing tail
+    uint8_t head_before = head_.load(std::memory_order_acquire);
+
+    if (next_tail == head_before) {
+      // Buffer full
+      dropped_count_.fetch_add(1, std::memory_order_relaxed);
+      return false;
+    }
+
+    was_empty = (current_tail == head_before);
+    old_tail = current_tail;
+
+    buffer_[current_tail] = element;
+    tail_.store(next_tail, std::memory_order_release);
+
+    return true;
+  }
+
+ public:
+  T *pop() {
+    uint8_t current_head = head_.load(std::memory_order_relaxed);
+
+    if (current_head == tail_.load(std::memory_order_acquire)) {
+      return nullptr;  // Empty
+    }
+
+    T *element = buffer_[current_head];
+    head_.store((current_head + 1) % SIZE, std::memory_order_release);
+    return element;
+  }
+
+  size_t size() const {
+    uint8_t tail = tail_.load(std::memory_order_acquire);
+    uint8_t head = head_.load(std::memory_order_acquire);
+    return (tail - head + SIZE) % SIZE;
+  }
+
+  uint16_t get_and_reset_dropped_count() { return dropped_count_.exchange(0, std::memory_order_relaxed); }
+
+  void increment_dropped_count() { dropped_count_.fetch_add(1, std::memory_order_relaxed); }
+
+  bool empty() const { return head_.load(std::memory_order_acquire) == tail_.load(std::memory_order_acquire); }
+
+  bool full() const {
+    uint8_t next_tail = (tail_.load(std::memory_order_relaxed) + 1) % SIZE;
+    return next_tail == head_.load(std::memory_order_acquire);
+  }
+
+ protected:
+  T *buffer_[SIZE];
+  // Atomic: written by producer (push/increment), read+reset by consumer (get_and_reset)
+  std::atomic dropped_count_;  // 65535 max - more than enough for drop tracking
+  // Atomic: written by consumer (pop), read by producer (push) to check if full
+  // Using uint8_t limits queue size to 255 elements but saves memory and ensures
+  // atomic operations are efficient on all platforms
+  std::atomic head_;
+  // Atomic: written by producer (push), read by consumer (pop) to check if empty
+  std::atomic tail_;
+};
+
+// Extended queue with task notification support
+template class NotifyingLockFreeQueue : public LockFreeQueue {
+ public:
+  NotifyingLockFreeQueue() : LockFreeQueue(), task_to_notify_(nullptr) {}
+
+  bool push(T *element) {
+    bool was_empty;
+    uint8_t old_tail;
+    bool result = this->push_internal_(element, was_empty, old_tail);
+
+    // Notify optimization: only notify if we need to
+    if (result && task_to_notify_ != nullptr &&
+        (was_empty || this->head_.load(std::memory_order_acquire) == old_tail)) {
+      // Notify in two cases:
+      // 1. Queue was empty - consumer might be going to sleep
+      // 2. Consumer just caught up to where tail was - might go to sleep
+      // Note: There's a benign race in case 2 - between reading head and calling
+      // xTaskNotifyGive(), the consumer could advance further. This would result
+      // in an unnecessary wake-up, but is harmless and extremely rare in practice.
+      xTaskNotifyGive(task_to_notify_);
+    }
+    // Otherwise: consumer is still behind, no need to notify
+
+    return result;
+  }
+
+  // Set the FreeRTOS task handle to notify when items are pushed to the queue
+  // This enables efficient wake-up of a consumer task that's waiting for data
+  // @param task The FreeRTOS task handle to notify, or nullptr to disable notifications
+  void set_task_to_notify(TaskHandle_t task) { task_to_notify_ = task; }
+
+ private:
+  TaskHandle_t task_to_notify_;
+};
+
+}  // namespace esphome
+
+#endif  // defined(USE_ESP32) || defined(USE_LIBRETINY)
diff --git a/esphome/core/log.cpp b/esphome/core/log.cpp
index 424154d253..909319dd28 100644
--- a/esphome/core/log.cpp
+++ b/esphome/core/log.cpp
@@ -29,7 +29,7 @@ void HOT esp_log_vprintf_(int level, const char *tag, int line, const char *form
   if (log == nullptr)
     return;
 
-  log->log_vprintf_(level, tag, line, format, args);
+  log->log_vprintf_(static_cast(level), tag, line, format, args);
 #endif
 }
 
@@ -41,7 +41,7 @@ void HOT esp_log_vprintf_(int level, const char *tag, int line, const __FlashStr
   if (log == nullptr)
     return;
 
-  log->log_vprintf_(level, tag, line, format, args);
+  log->log_vprintf_(static_cast(level), tag, line, format, args);
 #endif
 }
 #endif
diff --git a/esphome/core/log.h b/esphome/core/log.h
index adf72e4bac..cade6a74c1 100644
--- a/esphome/core/log.h
+++ b/esphome/core/log.h
@@ -165,6 +165,8 @@ int esp_idf_log_vprintf_(const char *format, va_list args);  // NOLINT
 #define YESNO(b) ((b) ? "YES" : "NO")
 #define ONOFF(b) ((b) ? "ON" : "OFF")
 #define TRUEFALSE(b) ((b) ? "TRUE" : "FALSE")
+// for use with optional values
+#define ONOFFMAYBE(b) (((b).has_value()) ? ONOFF((b).value()) : "UNKNOWN")
 
 // Helper class that identifies strings that may be stored in flash storage (similar to Arduino's __FlashStringHelper)
 struct LogString;
diff --git a/esphome/core/optional.h b/esphome/core/optional.h
index 591bc7aa68..7f9db7817d 100644
--- a/esphome/core/optional.h
+++ b/esphome/core/optional.h
@@ -52,6 +52,11 @@ template class optional {  // NOLINT
     reset();
     return *this;
   }
+  bool operator==(optional const &rhs) const {
+    if (has_value() && rhs.has_value())
+      return value() == rhs.value();
+    return !has_value() && !rhs.has_value();
+  }
 
   template optional &operator=(optional const &other) {
     has_value_ = other.has_value();
diff --git a/esphome/core/ring_buffer.cpp b/esphome/core/ring_buffer.cpp
index f779531263..b77a02b2a7 100644
--- a/esphome/core/ring_buffer.cpp
+++ b/esphome/core/ring_buffer.cpp
@@ -14,7 +14,7 @@ static const char *const TAG = "ring_buffer";
 RingBuffer::~RingBuffer() {
   if (this->handle_ != nullptr) {
     vRingbufferDelete(this->handle_);
-    RAMAllocator allocator(RAMAllocator::ALLOW_FAILURE);
+    RAMAllocator allocator;
     allocator.deallocate(this->storage_, this->size_);
   }
 }
@@ -24,7 +24,7 @@ std::unique_ptr RingBuffer::create(size_t len) {
 
   rb->size_ = len;
 
-  RAMAllocator allocator(RAMAllocator::ALLOW_FAILURE);
+  RAMAllocator allocator;
   rb->storage_ = allocator.allocate(rb->size_);
   if (rb->storage_ == nullptr) {
     return nullptr;
diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp
index eed222c974..d3da003a88 100644
--- a/esphome/core/scheduler.cpp
+++ b/esphome/core/scheduler.cpp
@@ -7,6 +7,7 @@
 #include "esphome/core/log.h"
 #include 
 #include 
+#include 
 
 namespace esphome {
 
@@ -17,67 +18,150 @@ static const uint32_t MAX_LOGICALLY_DELETED_ITEMS = 10;
 // Uncomment to debug scheduler
 // #define ESPHOME_DEBUG_SCHEDULER
 
+#ifdef ESPHOME_DEBUG_SCHEDULER
+// Helper to validate that a pointer looks like it's in static memory
+static void validate_static_string(const char *name) {
+  if (name == nullptr)
+    return;
+
+  // This is a heuristic check - stack and heap pointers are typically
+  // much higher in memory than static data
+  uintptr_t addr = reinterpret_cast(name);
+
+  // Create a stack variable to compare against
+  int stack_var;
+  uintptr_t stack_addr = reinterpret_cast(&stack_var);
+
+  // If the string pointer is near our stack variable, it's likely on the stack
+  // Using 8KB range as ESP32 main task stack is typically 8192 bytes
+  if (addr > (stack_addr - 0x2000) && addr < (stack_addr + 0x2000)) {
+    ESP_LOGW(TAG,
+             "WARNING: Scheduler name '%s' at %p appears to be on the stack - this is unsafe!\n"
+             "         Stack reference at %p",
+             name, name, &stack_var);
+  }
+
+  // Also check if it might be on the heap by seeing if it's in a very different range
+  // This is platform-specific but generally heap is allocated far from static memory
+  static const char *static_str = "test";
+  uintptr_t static_addr = reinterpret_cast(static_str);
+
+  // If the address is very far from known static memory, it might be heap
+  if (addr > static_addr + 0x100000 || (static_addr > 0x100000 && addr < static_addr - 0x100000)) {
+    ESP_LOGW(TAG, "WARNING: Scheduler name '%s' at %p might be on heap (static ref at %p)", name, name, static_str);
+  }
+}
+#endif
+
 // A note on locking: the `lock_` lock protects the `items_` and `to_add_` containers. It must be taken when writing to
 // them (i.e. when adding/removing items, but not when changing items). As items are only deleted from the loop task,
 // iterating over them from the loop task is fine; but iterating from any other context requires the lock to be held to
 // avoid the main thread modifying the list while it is being accessed.
 
-void HOT Scheduler::set_timeout(Component *component, const std::string &name, uint32_t timeout,
-                                std::function func) {
-  const auto now = this->millis_();
+// Common implementation for both timeout and interval
+void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type type, bool is_static_string,
+                                      const void *name_ptr, uint32_t delay, std::function func) {
+  // Get the name as const char*
+  const char *name_cstr = this->get_name_cstr_(is_static_string, name_ptr);
 
-  if (!name.empty())
-    this->cancel_timeout(component, name);
-
-  if (timeout == SCHEDULER_DONT_RUN)
+  if (delay == SCHEDULER_DONT_RUN) {
+    // Still need to cancel existing timer if name is not empty
+    if (this->is_name_valid_(name_cstr)) {
+      LockGuard guard{this->lock_};
+      this->cancel_item_locked_(component, name_cstr, type);
+    }
     return;
+  }
 
+  // Create and populate the scheduler item
   auto item = make_unique();
   item->component = component;
-  item->name = name;
-  item->type = SchedulerItem::TIMEOUT;
-  item->next_execution_ = now + timeout;
+  item->set_name(name_cstr, !is_static_string);
+  item->type = type;
   item->callback = std::move(func);
   item->remove = false;
-#ifdef ESPHOME_DEBUG_SCHEDULER
-  ESP_LOGD(TAG, "set_timeout(name='%s/%s', timeout=%" PRIu32 ")", item->get_source(), name.c_str(), timeout);
+
+#if !defined(USE_ESP8266) && !defined(USE_RP2040)
+  // Special handling for defer() (delay = 0, type = TIMEOUT)
+  // ESP8266 and RP2040 are excluded because they don't need thread-safe defer handling
+  if (delay == 0 && type == SchedulerItem::TIMEOUT) {
+    // Put in defer queue for guaranteed FIFO execution
+    LockGuard guard{this->lock_};
+    this->cancel_item_locked_(component, name_cstr, type);
+    this->defer_queue_.push_back(std::move(item));
+    return;
+  }
 #endif
-  this->push_(std::move(item));
+
+  const auto now = this->millis_();
+
+  // Type-specific setup
+  if (type == SchedulerItem::INTERVAL) {
+    item->interval = delay;
+    // Calculate random offset (0 to interval/2)
+    uint32_t offset = (delay != 0) ? (random_uint32() % delay) / 2 : 0;
+    item->next_execution_ = now + offset;
+  } else {
+    item->interval = 0;
+    item->next_execution_ = now + delay;
+  }
+
+#ifdef ESPHOME_DEBUG_SCHEDULER
+  // Validate static strings in debug mode
+  if (is_static_string && name_cstr != nullptr) {
+    validate_static_string(name_cstr);
+  }
+
+  // Debug logging
+  const char *type_str = (type == SchedulerItem::TIMEOUT) ? "timeout" : "interval";
+  if (type == SchedulerItem::TIMEOUT) {
+    ESP_LOGD(TAG, "set_%s(name='%s/%s', %s=%" PRIu32 ")", type_str, item->get_source(),
+             name_cstr ? name_cstr : "(null)", type_str, delay);
+  } else {
+    ESP_LOGD(TAG, "set_%s(name='%s/%s', %s=%" PRIu32 ", offset=%" PRIu32 ")", type_str, item->get_source(),
+             name_cstr ? name_cstr : "(null)", type_str, delay, static_cast(item->next_execution_ - now));
+  }
+#endif
+
+  LockGuard guard{this->lock_};
+  // If name is provided, do atomic cancel-and-add
+  if (this->is_name_valid_(name_cstr)) {
+    // Cancel existing items
+    this->cancel_item_locked_(component, name_cstr, type);
+  }
+  // Add new item directly to to_add_
+  // since we have the lock held
+  this->to_add_.push_back(std::move(item));
+}
+
+void HOT Scheduler::set_timeout(Component *component, const char *name, uint32_t timeout, std::function func) {
+  this->set_timer_common_(component, SchedulerItem::TIMEOUT, true, name, timeout, std::move(func));
+}
+
+void HOT Scheduler::set_timeout(Component *component, const std::string &name, uint32_t timeout,
+                                std::function func) {
+  this->set_timer_common_(component, SchedulerItem::TIMEOUT, false, &name, timeout, std::move(func));
 }
 bool HOT Scheduler::cancel_timeout(Component *component, const std::string &name) {
-  return this->cancel_item_(component, name, SchedulerItem::TIMEOUT);
+  return this->cancel_item_(component, false, &name, SchedulerItem::TIMEOUT);
+}
+bool HOT Scheduler::cancel_timeout(Component *component, const char *name) {
+  return this->cancel_item_(component, true, name, SchedulerItem::TIMEOUT);
 }
 void HOT Scheduler::set_interval(Component *component, const std::string &name, uint32_t interval,
                                  std::function func) {
-  const auto now = this->millis_();
+  this->set_timer_common_(component, SchedulerItem::INTERVAL, false, &name, interval, std::move(func));
+}
 
-  if (!name.empty())
-    this->cancel_interval(component, name);
-
-  if (interval == SCHEDULER_DONT_RUN)
-    return;
-
-  // only put offset in lower half
-  uint32_t offset = 0;
-  if (interval != 0)
-    offset = (random_uint32() % interval) / 2;
-
-  auto item = make_unique();
-  item->component = component;
-  item->name = name;
-  item->type = SchedulerItem::INTERVAL;
-  item->interval = interval;
-  item->next_execution_ = now + offset;
-  item->callback = std::move(func);
-  item->remove = false;
-#ifdef ESPHOME_DEBUG_SCHEDULER
-  ESP_LOGD(TAG, "set_interval(name='%s/%s', interval=%" PRIu32 ", offset=%" PRIu32 ")", item->get_source(),
-           name.c_str(), interval, offset);
-#endif
-  this->push_(std::move(item));
+void HOT Scheduler::set_interval(Component *component, const char *name, uint32_t interval,
+                                 std::function func) {
+  this->set_timer_common_(component, SchedulerItem::INTERVAL, true, name, interval, std::move(func));
 }
 bool HOT Scheduler::cancel_interval(Component *component, const std::string &name) {
-  return this->cancel_item_(component, name, SchedulerItem::INTERVAL);
+  return this->cancel_item_(component, false, &name, SchedulerItem::INTERVAL);
+}
+bool HOT Scheduler::cancel_interval(Component *component, const char *name) {
+  return this->cancel_item_(component, true, name, SchedulerItem::INTERVAL);
 }
 
 struct RetryArgs {
@@ -85,7 +169,7 @@ struct RetryArgs {
   uint8_t retry_countdown;
   uint32_t current_interval;
   Component *component;
-  std::string name;
+  std::string name;  // Keep as std::string since retry uses it dynamically
   float backoff_increase_factor;
   Scheduler *scheduler;
 };
@@ -136,6 +220,9 @@ bool HOT Scheduler::cancel_retry(Component *component, const std::string &name)
 }
 
 optional HOT Scheduler::next_schedule_in() {
+  // IMPORTANT: This method should only be called from the main thread (loop task).
+  // It calls empty_() and accesses items_[0] without holding a lock, which is only
+  // safe when called from the main thread. Other threads must not call this method.
   if (this->empty_())
     return {};
   auto &item = this->items_[0];
@@ -145,6 +232,39 @@ optional HOT Scheduler::next_schedule_in() {
   return item->next_execution_ - now;
 }
 void HOT Scheduler::call() {
+#if !defined(USE_ESP8266) && !defined(USE_RP2040)
+  // Process defer queue first to guarantee FIFO execution order for deferred items.
+  // Previously, defer() used the heap which gave undefined order for equal timestamps,
+  // causing race conditions on multi-core systems (ESP32, BK7200).
+  // With the defer queue:
+  // - Deferred items (delay=0) go directly to defer_queue_ in set_timer_common_
+  // - Items execute in exact order they were deferred (FIFO guarantee)
+  // - No deferred items exist in to_add_, so processing order doesn't affect correctness
+  // ESP8266 and RP2040 don't use this queue - they fall back to the heap-based approach
+  // (ESP8266: single-core, RP2040: empty mutex implementation).
+  //
+  // Note: Items cancelled via cancel_item_locked_() are marked with remove=true but still
+  // processed here. They are removed from the queue normally via pop_front() but skipped
+  // during execution by should_skip_item_(). This is intentional - no memory leak occurs.
+  while (!this->defer_queue_.empty()) {
+    // The outer check is done without a lock for performance. If the queue
+    // appears non-empty, we lock and process an item. We don't need to check
+    // empty() again inside the lock because only this thread can remove items.
+    std::unique_ptr item;
+    {
+      LockGuard lock(this->lock_);
+      item = std::move(this->defer_queue_.front());
+      this->defer_queue_.pop_front();
+    }
+
+    // Execute callback without holding lock to prevent deadlocks
+    // if the callback tries to call defer() again
+    if (!this->should_skip_item_(item.get())) {
+      this->execute_item_(item.get());
+    }
+  }
+#endif
+
   const auto now = this->millis_();
   this->process_to_add();
 
@@ -154,16 +274,19 @@ void HOT Scheduler::call() {
   if (now - last_print > 2000) {
     last_print = now;
     std::vector> old_items;
-    ESP_LOGD(TAG, "Items: count=%u, now=%" PRIu64 " (%u, %" PRIu32 ")", this->items_.size(), now, this->millis_major_,
+    ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%u, %" PRIu32 ")", this->items_.size(), now, this->millis_major_,
              this->last_millis_);
     while (!this->empty_()) {
-      this->lock_.lock();
-      auto item = std::move(this->items_[0]);
-      this->pop_raw_();
-      this->lock_.unlock();
+      std::unique_ptr item;
+      {
+        LockGuard guard{this->lock_};
+        item = std::move(this->items_[0]);
+        this->pop_raw_();
+      }
 
+      const char *name = item->get_name();
       ESP_LOGD(TAG, "  %s '%s/%s' interval=%" PRIu32 " next_execution in %" PRIu64 "ms at %" PRIu64,
-               item->get_type_str(), item->get_source(), item->name.c_str(), item->interval,
+               item->get_type_str(), item->get_source(), name ? name : "(null)", item->interval,
                item->next_execution_ - now, item->next_execution_);
 
       old_items.push_back(std::move(item));
@@ -173,33 +296,35 @@ void HOT Scheduler::call() {
     {
       LockGuard guard{this->lock_};
       this->items_ = std::move(old_items);
+      // Rebuild heap after moving items back
+      std::make_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp);
     }
   }
 #endif  // ESPHOME_DEBUG_SCHEDULER
 
-  auto to_remove_was = to_remove_;
-  auto items_was = this->items_.size();
   // If we have too many items to remove
-  if (to_remove_ > MAX_LOGICALLY_DELETED_ITEMS) {
+  if (this->to_remove_ > MAX_LOGICALLY_DELETED_ITEMS) {
+    // We hold the lock for the entire cleanup operation because:
+    // 1. We're rebuilding the entire items_ list, so we need exclusive access throughout
+    // 2. Other threads must see either the old state or the new state, not intermediate states
+    // 3. The operation is already expensive (O(n)), so lock overhead is negligible
+    // 4. No operations inside can block or take other locks, so no deadlock risk
+    LockGuard guard{this->lock_};
+
     std::vector> valid_items;
-    while (!this->empty_()) {
-      LockGuard guard{this->lock_};
-      auto item = std::move(this->items_[0]);
-      this->pop_raw_();
-      valid_items.push_back(std::move(item));
+
+    // Move all non-removed items to valid_items
+    for (auto &item : this->items_) {
+      if (!item->remove) {
+        valid_items.push_back(std::move(item));
+      }
     }
 
-    {
-      LockGuard guard{this->lock_};
-      this->items_ = std::move(valid_items);
-    }
-
-    // The following should not happen unless I'm missing something
-    if (to_remove_ != 0) {
-      ESP_LOGW(TAG, "to_remove_ was %" PRIu32 " now: %" PRIu32 " items where %zu now %zu. Please report this",
-               to_remove_was, to_remove_, items_was, items_.size());
-      to_remove_ = 0;
-    }
+    // Replace items_ with the filtered list
+    this->items_ = std::move(valid_items);
+    // Rebuild the heap structure since items are no longer in heap order
+    std::make_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp);
+    this->to_remove_ = 0;
   }
 
   while (!this->empty_()) {
@@ -217,47 +342,39 @@ void HOT Scheduler::call() {
         this->pop_raw_();
         continue;
       }
-      App.set_current_component(item->component);
-
 #ifdef ESPHOME_DEBUG_SCHEDULER
+      const char *item_name = item->get_name();
       ESP_LOGV(TAG, "Running %s '%s/%s' with interval=%" PRIu32 " next_execution=%" PRIu64 " (now=%" PRIu64 ")",
-               item->get_type_str(), item->get_source(), item->name.c_str(), item->interval, item->next_execution_,
-               now);
+               item->get_type_str(), item->get_source(), item_name ? item_name : "(null)", item->interval,
+               item->next_execution_, now);
 #endif
 
       // Warning: During callback(), a lot of stuff can happen, including:
       //  - timeouts/intervals get added, potentially invalidating vector pointers
       //  - timeouts/intervals get cancelled
-      {
-        uint32_t now_ms = millis();
-        WarnIfComponentBlockingGuard guard{item->component, now_ms};
-        item->callback();
-        // Call finish to ensure blocking time is properly calculated and reported
-        guard.finish();
-      }
+      this->execute_item_(item.get());
     }
 
     {
-      this->lock_.lock();
+      LockGuard guard{this->lock_};
 
       // new scope, item from before might have been moved in the vector
       auto item = std::move(this->items_[0]);
-
       // Only pop after function call, this ensures we were reachable
       // during the function call and know if we were cancelled.
       this->pop_raw_();
 
-      this->lock_.unlock();
-
       if (item->remove) {
         // We were removed/cancelled in the function call, stop
-        to_remove_--;
+        this->to_remove_--;
         continue;
       }
 
       if (item->type == SchedulerItem::INTERVAL) {
         item->next_execution_ = now + item->interval;
-        this->push_(std::move(item));
+        // Add new item directly to to_add_
+        // since we have the lock held
+        this->to_add_.push_back(std::move(item));
       }
     }
   }
@@ -277,55 +394,113 @@ void HOT Scheduler::process_to_add() {
   this->to_add_.clear();
 }
 void HOT Scheduler::cleanup_() {
+  // Fast path: if nothing to remove, just return
+  // Reading to_remove_ without lock is safe because:
+  // 1. We only call this from the main thread during call()
+  // 2. If it's 0, there's definitely nothing to cleanup
+  // 3. If it becomes non-zero after we check, cleanup will happen on the next loop iteration
+  // 4. Not all platforms support atomics, so we accept this race in favor of performance
+  // 5. The worst case is a one-loop-iteration delay in cleanup, which is harmless
+  if (this->to_remove_ == 0)
+    return;
+
+  // We must hold the lock for the entire cleanup operation because:
+  // 1. We're modifying items_ (via pop_raw_) which requires exclusive access
+  // 2. We're decrementing to_remove_ which is also modified by other threads
+  //    (though all modifications are already under lock)
+  // 3. Other threads read items_ when searching for items to cancel in cancel_item_locked_()
+  // 4. We need a consistent view of items_ and to_remove_ throughout the operation
+  // Without the lock, we could access items_ while another thread is reading it,
+  // leading to race conditions
+  LockGuard guard{this->lock_};
   while (!this->items_.empty()) {
     auto &item = this->items_[0];
     if (!item->remove)
       return;
-
-    to_remove_--;
-
-    {
-      LockGuard guard{this->lock_};
-      this->pop_raw_();
-    }
+    this->to_remove_--;
+    this->pop_raw_();
   }
 }
 void HOT Scheduler::pop_raw_() {
   std::pop_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp);
   this->items_.pop_back();
 }
-void HOT Scheduler::push_(std::unique_ptr item) {
-  LockGuard guard{this->lock_};
-  this->to_add_.push_back(std::move(item));
+
+// Helper to execute a scheduler item
+void HOT Scheduler::execute_item_(SchedulerItem *item) {
+  App.set_current_component(item->component);
+
+  uint32_t now_ms = millis();
+  WarnIfComponentBlockingGuard guard{item->component, now_ms};
+  item->callback();
+  guard.finish();
 }
-bool HOT Scheduler::cancel_item_(Component *component, const std::string &name, Scheduler::SchedulerItem::Type type) {
+
+// Common implementation for cancel operations
+bool HOT Scheduler::cancel_item_(Component *component, bool is_static_string, const void *name_ptr,
+                                 SchedulerItem::Type type) {
+  // Get the name as const char*
+  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
   LockGuard guard{this->lock_};
-  bool ret = false;
-  for (auto &it : this->items_) {
-    if (it->component == component && it->name == name && it->type == type && !it->remove) {
-      to_remove_++;
-      it->remove = true;
-      ret = true;
+  return this->cancel_item_locked_(component, name_cstr, type);
+}
+
+// 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) {
+  size_t total_cancelled = 0;
+
+  // Check all containers for matching items
+#if !defined(USE_ESP8266) && !defined(USE_RP2040)
+  // Only check defer queue for timeouts (intervals never go there)
+  if (type == SchedulerItem::TIMEOUT) {
+    for (auto &item : this->defer_queue_) {
+      if (this->matches_item_(item, component, name_cstr, type)) {
+        item->remove = true;
+        total_cancelled++;
+      }
     }
   }
-  for (auto &it : this->to_add_) {
-    if (it->component == component && it->name == name && it->type == type) {
-      it->remove = true;
-      ret = true;
+#endif
+
+  // Cancel items in the main heap
+  for (auto &item : this->items_) {
+    if (this->matches_item_(item, component, name_cstr, type)) {
+      item->remove = true;
+      total_cancelled++;
+      this->to_remove_++;  // Track removals for heap items
     }
   }
 
-  return ret;
+  // Cancel items in to_add_
+  for (auto &item : this->to_add_) {
+    if (this->matches_item_(item, component, name_cstr, type)) {
+      item->remove = true;
+      total_cancelled++;
+      // Don't track removals for to_add_ items
+    }
+  }
+
+  return total_cancelled > 0;
 }
+
 uint64_t Scheduler::millis_() {
+  // Get the current 32-bit millis value
   const uint32_t now = millis();
+  // Check for rollover by comparing with last value
   if (now < this->last_millis_) {
+    // Detected rollover (happens every ~49.7 days)
     this->millis_major_++;
     ESP_LOGD(TAG, "Incrementing scheduler major at %" PRIu64 "ms",
              now + (static_cast(this->millis_major_) << 32));
   }
   this->last_millis_ = now;
+  // Combine major (high 32 bits) and now (low 32 bits) into 64-bit time
   return now + (static_cast(this->millis_major_) << 32);
 }
 
diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h
index 872a8bd6f6..084ff699c5 100644
--- a/esphome/core/scheduler.h
+++ b/esphome/core/scheduler.h
@@ -2,6 +2,8 @@
 
 #include 
 #include 
+#include 
+#include 
 
 #include "esphome/core/component.h"
 #include "esphome/core/helpers.h"
@@ -12,11 +14,40 @@ class Component;
 
 class Scheduler {
  public:
+  // Public API - accepts std::string for backward compatibility
   void set_timeout(Component *component, const std::string &name, uint32_t timeout, std::function func);
-  bool cancel_timeout(Component *component, const std::string &name);
-  void set_interval(Component *component, const std::string &name, uint32_t interval, std::function func);
-  bool cancel_interval(Component *component, const std::string &name);
 
+  /** Set a timeout with a const char* name.
+   *
+   * IMPORTANT: The provided name pointer must remain valid for the lifetime of the scheduler item.
+   * This means the name should be:
+   *   - A string literal (e.g., "update")
+   *   - A static const char* variable
+   *   - A pointer with lifetime >= the scheduled task
+   *
+   * For dynamic strings, use the std::string overload instead.
+   */
+  void set_timeout(Component *component, const char *name, uint32_t timeout, std::function func);
+
+  bool cancel_timeout(Component *component, const std::string &name);
+  bool cancel_timeout(Component *component, const char *name);
+
+  void set_interval(Component *component, const std::string &name, uint32_t interval, std::function func);
+
+  /** Set an interval with a const char* name.
+   *
+   * IMPORTANT: The provided name pointer must remain valid for the lifetime of the scheduler item.
+   * This means the name should be:
+   *   - A string literal (e.g., "update")
+   *   - A static const char* variable
+   *   - A pointer with lifetime >= the scheduled task
+   *
+   * For dynamic strings, use the std::string overload instead.
+   */
+  void set_interval(Component *component, const char *name, uint32_t interval, std::function func);
+
+  bool cancel_interval(Component *component, const std::string &name);
+  bool cancel_interval(Component *component, const char *name);
   void set_retry(Component *component, const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts,
                  std::function func, float backoff_increase_factor = 1.0f);
   bool cancel_retry(Component *component, const std::string &name);
@@ -29,35 +60,137 @@ class Scheduler {
 
  protected:
   struct SchedulerItem {
+    // Ordered by size to minimize padding
     Component *component;
-    std::string name;
-    enum Type { TIMEOUT, INTERVAL } type;
     uint32_t interval;
+    // 64-bit time to handle millis() rollover. The scheduler combines the 32-bit millis()
+    // with a 16-bit rollover counter to create a 64-bit time that won't roll over for
+    // billions of years. This ensures correct scheduling even when devices run for months.
     uint64_t next_execution_;
-    std::function callback;
-    bool remove;
 
-    static bool cmp(const std::unique_ptr &a, const std::unique_ptr &b);
-    const char *get_type_str() {
-      switch (this->type) {
-        case SchedulerItem::INTERVAL:
-          return "interval";
-        case SchedulerItem::TIMEOUT:
-          return "timeout";
-        default:
-          return "";
+    // Optimized name storage using tagged union
+    union {
+      const char *static_name;  // For string literals (no allocation)
+      char *dynamic_name;       // For allocated strings
+    } name_;
+
+    std::function callback;
+
+    // Bit-packed fields to minimize padding
+    enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1;
+    bool remove : 1;
+    bool name_is_dynamic : 1;  // True if name was dynamically allocated (needs delete[])
+    // 5 bits padding
+
+    // Constructor
+    SchedulerItem()
+        : component(nullptr), interval(0), next_execution_(0), type(TIMEOUT), remove(false), name_is_dynamic(false) {
+      name_.static_name = nullptr;
+    }
+
+    // Destructor to clean up dynamic names
+    ~SchedulerItem() {
+      if (name_is_dynamic) {
+        delete[] name_.dynamic_name;
       }
     }
-    const char *get_source() {
-      return this->component != nullptr ? this->component->get_component_source() : "unknown";
+
+    // Delete copy operations to prevent accidental copies
+    SchedulerItem(const SchedulerItem &) = delete;
+    SchedulerItem &operator=(const SchedulerItem &) = delete;
+
+    // Delete move operations: SchedulerItem objects are only managed via unique_ptr, never moved directly
+    SchedulerItem(SchedulerItem &&) = delete;
+    SchedulerItem &operator=(SchedulerItem &&) = delete;
+
+    // Helper to get the name regardless of storage type
+    const char *get_name() const { return name_is_dynamic ? name_.dynamic_name : name_.static_name; }
+
+    // Helper to set name with proper ownership
+    void set_name(const char *name, bool make_copy = false) {
+      // Clean up old dynamic name if any
+      if (name_is_dynamic && name_.dynamic_name) {
+        delete[] name_.dynamic_name;
+        name_is_dynamic = false;
+      }
+
+      if (!name || !name[0]) {
+        name_.static_name = nullptr;
+      } else if (make_copy) {
+        // Make a copy for dynamic strings
+        size_t len = strlen(name);
+        name_.dynamic_name = new char[len + 1];
+        memcpy(name_.dynamic_name, name, len + 1);
+        name_is_dynamic = true;
+      } else {
+        // Use static string directly
+        name_.static_name = name;
+      }
     }
+
+    static bool cmp(const std::unique_ptr &a, const std::unique_ptr &b);
+    const char *get_type_str() const { return (type == TIMEOUT) ? "timeout" : "interval"; }
+    const char *get_source() const { return component ? component->get_component_source() : "unknown"; }
   };
 
+  // Common implementation for both timeout and interval
+  void set_timer_common_(Component *component, SchedulerItem::Type type, bool is_static_string, const void *name_ptr,
+                         uint32_t delay, std::function func);
+
   uint64_t millis_();
   void cleanup_();
   void pop_raw_();
-  void push_(std::unique_ptr item);
-  bool cancel_item_(Component *component, const std::string &name, SchedulerItem::Type type);
+
+ private:
+  // Helper to cancel items by name - must be called with lock held
+  bool cancel_item_locked_(Component *component, const char *name, SchedulerItem::Type type);
+
+  // Helper to extract name as const char* from either static string or std::string
+  inline const char *get_name_cstr_(bool is_static_string, const void *name_ptr) {
+    return is_static_string ? static_cast(name_ptr) : static_cast(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
+  bool cancel_item_(Component *component, bool is_static_string, const void *name_ptr, SchedulerItem::Type type);
+
+  // Helper function to check if item matches criteria for cancellation
+  inline bool HOT matches_item_(const std::unique_ptr &item, Component *component, const char *name_cstr,
+                                SchedulerItem::Type type) {
+    if (item->component != component || item->type != type || item->remove) {
+      return false;
+    }
+    const char *item_name = item->get_name();
+    if (item_name == nullptr) {
+      return false;
+    }
+    // Fast path: if pointers are equal
+    // This is effective because the core ESPHome codebase uses static strings (const char*)
+    // for component names. The std::string overloads exist only for compatibility with
+    // external components, but are rarely used in practice.
+    if (item_name == name_cstr) {
+      return true;
+    }
+    // Slow path: compare string contents
+    return strcmp(name_cstr, item_name) == 0;
+  }
+
+  // Helper to execute a scheduler item
+  void execute_item_(SchedulerItem *item);
+
+  // Helper to check if item should be skipped
+  bool should_skip_item_(const SchedulerItem *item) const {
+    return item->remove || (item->component != nullptr && item->component->is_failed());
+  }
+
+  // Check if the scheduler has no items.
+  // IMPORTANT: This method should only be called from the main thread (loop task).
+  // It performs cleanup of removed items and checks if the queue is empty.
+  // The items_.empty() check at the end is done without a lock for performance,
+  // which is safe because this is only called from the main thread while other
+  // threads only add items (never remove them).
   bool empty_() {
     this->cleanup_();
     return this->items_.empty();
@@ -66,6 +199,13 @@ class Scheduler {
   Mutex lock_;
   std::vector> items_;
   std::vector> to_add_;
+#if !defined(USE_ESP8266) && !defined(USE_RP2040)
+  // ESP8266 and RP2040 don't need the defer queue because:
+  // ESP8266: Single-core with no preemptive multitasking
+  // RP2040: Currently has empty mutex implementation in ESPHome
+  // Both platforms save 40 bytes of RAM by excluding this
+  std::deque> defer_queue_;  // FIFO queue for defer() calls
+#endif
   uint32_t last_millis_{0};
   uint16_t millis_major_{0};
   uint32_t to_remove_{0};
diff --git a/esphome/core/time.cpp b/esphome/core/time.cpp
index 672f5b98bf..f9652b5329 100644
--- a/esphome/core/time.cpp
+++ b/esphome/core/time.cpp
@@ -226,11 +226,11 @@ int32_t ESPTime::timezone_offset() {
   return offset;
 }
 
-bool ESPTime::operator<(ESPTime other) { return this->timestamp < other.timestamp; }
-bool ESPTime::operator<=(ESPTime other) { return this->timestamp <= other.timestamp; }
-bool ESPTime::operator==(ESPTime other) { return this->timestamp == other.timestamp; }
-bool ESPTime::operator>=(ESPTime other) { return this->timestamp >= other.timestamp; }
-bool ESPTime::operator>(ESPTime other) { return this->timestamp > other.timestamp; }
+bool ESPTime::operator<(const ESPTime &other) const { return this->timestamp < other.timestamp; }
+bool ESPTime::operator<=(const ESPTime &other) const { return this->timestamp <= other.timestamp; }
+bool ESPTime::operator==(const ESPTime &other) const { return this->timestamp == other.timestamp; }
+bool ESPTime::operator>=(const ESPTime &other) const { return this->timestamp >= other.timestamp; }
+bool ESPTime::operator>(const ESPTime &other) const { return this->timestamp > other.timestamp; }
 
 template bool increment_time_value(T ¤t, uint16_t begin, uint16_t end) {
   current++;
diff --git a/esphome/core/time.h b/esphome/core/time.h
index 5cbd9369fb..a53fca2346 100644
--- a/esphome/core/time.h
+++ b/esphome/core/time.h
@@ -109,10 +109,10 @@ struct ESPTime {
   void increment_second();
   /// Increment this clock instance by one day.
   void increment_day();
-  bool operator<(ESPTime other);
-  bool operator<=(ESPTime other);
-  bool operator==(ESPTime other);
-  bool operator>=(ESPTime other);
-  bool operator>(ESPTime other);
+  bool operator<(const ESPTime &other) const;
+  bool operator<=(const ESPTime &other) const;
+  bool operator==(const ESPTime &other) const;
+  bool operator>=(const ESPTime &other) const;
+  bool operator>(const ESPTime &other) const;
 };
 }  // namespace esphome
diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py
index bbfa6af815..060dd36f8f 100644
--- a/esphome/cpp_generator.py
+++ b/esphome/cpp_generator.py
@@ -608,6 +608,23 @@ def add_build_flag(build_flag: str):
     CORE.add_build_flag(build_flag)
 
 
+def add_build_unflag(build_unflag: str) -> None:
+    """Add a global build unflag to the compiler flags."""
+    CORE.add_build_unflag(build_unflag)
+
+
+def set_cpp_standard(standard: str) -> None:
+    """Set C++ standard with compiler flag `-std={standard}`."""
+    CORE.add_build_unflag("-std=gnu++11")
+    CORE.add_build_unflag("-std=gnu++14")
+    CORE.add_build_unflag("-std=gnu++17")
+    CORE.add_build_unflag("-std=gnu++23")
+    CORE.add_build_unflag("-std=gnu++2a")
+    CORE.add_build_unflag("-std=gnu++2b")
+    CORE.add_build_unflag("-std=gnu++2c")
+    CORE.add_build_flag(f"-std={standard}")
+
+
 def add_define(name: str, value: SafeExpType = None):
     """Add a global define to the auto-generated defines.h file.
 
diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py
index 9a775bad33..3f64be6154 100644
--- a/esphome/cpp_helpers.py
+++ b/esphome/cpp_helpers.py
@@ -1,11 +1,6 @@
 import logging
 
 from esphome.const import (
-    CONF_DISABLED_BY_DEFAULT,
-    CONF_ENTITY_CATEGORY,
-    CONF_ICON,
-    CONF_INTERNAL,
-    CONF_NAME,
     CONF_SAFE_MODE,
     CONF_SETUP_PRIORITY,
     CONF_TYPE_ID,
@@ -16,7 +11,6 @@ from esphome.core import CORE, ID, coroutine
 from esphome.coroutine import FakeAwaitable
 from esphome.cpp_generator import add, get_variable
 from esphome.cpp_types import App
-from esphome.helpers import sanitize, snake_case
 from esphome.types import ConfigFragmentType, ConfigType
 from esphome.util import Registry, RegistryEntry
 
@@ -96,22 +90,6 @@ async def register_parented(var, value):
     add(var.set_parent(paren))
 
 
-async def setup_entity(var, config):
-    """Set up generic properties of an Entity"""
-    add(var.set_name(config[CONF_NAME]))
-    if not config[CONF_NAME]:
-        add(var.set_object_id(sanitize(snake_case(CORE.friendly_name))))
-    else:
-        add(var.set_object_id(sanitize(snake_case(config[CONF_NAME]))))
-    add(var.set_disabled_by_default(config[CONF_DISABLED_BY_DEFAULT]))
-    if CONF_INTERNAL in config:
-        add(var.set_internal(config[CONF_INTERNAL]))
-    if CONF_ICON in config:
-        add(var.set_icon(config[CONF_ICON]))
-    if CONF_ENTITY_CATEGORY in config:
-        add(var.set_entity_category(config[CONF_ENTITY_CATEGORY]))
-
-
 def extract_registry_entry_config(
     registry: Registry,
     full_config: ConfigType,
diff --git a/esphome/cpp_types.py b/esphome/cpp_types.py
index dab993f87f..a0dd62cb4e 100644
--- a/esphome/cpp_types.py
+++ b/esphome/cpp_types.py
@@ -29,7 +29,9 @@ Component = esphome_ns.class_("Component")
 ComponentPtr = Component.operator("ptr")
 PollingComponent = esphome_ns.class_("PollingComponent", Component)
 Application = esphome_ns.class_("Application")
-optional = esphome_ns.class_("optional")
+# Create optional with explicit namespace to avoid ambiguity with std::optional
+# The generated code will use esphome::optional instead of just optional
+optional = global_ns.namespace("esphome").class_("optional")
 arduino_json_ns = global_ns.namespace("ArduinoJson")
 JsonObject = arduino_json_ns.class_("JsonObject")
 JsonObjectConst = arduino_json_ns.class_("JsonObjectConst")
diff --git a/esphome/dashboard/entries.py b/esphome/dashboard/entries.py
index e4825298f7..b138cfd272 100644
--- a/esphome/dashboard/entries.py
+++ b/esphome/dashboard/entries.py
@@ -9,6 +9,7 @@ import os
 from typing import TYPE_CHECKING, Any
 
 from esphome import const, util
+from esphome.enum import StrEnum
 from esphome.storage_json import StorageJSON, ext_storage_path
 
 from .const import (
@@ -18,7 +19,6 @@ from .const import (
     EVENT_ENTRY_STATE_CHANGED,
     EVENT_ENTRY_UPDATED,
 )
-from .enum import StrEnum
 from .util.subprocess import async_run_system_command
 
 if TYPE_CHECKING:
diff --git a/esphome/dashboard/util/text.py b/esphome/dashboard/util/text.py
index 08d2df6abf..2a3b9042e6 100644
--- a/esphome/dashboard/util/text.py
+++ b/esphome/dashboard/util/text.py
@@ -1,25 +1,9 @@
 from __future__ import annotations
 
-import unicodedata
-
-from esphome.const import ALLOWED_NAME_CHARS
+from esphome.helpers import slugify
 
 
-def strip_accents(value):
-    return "".join(
-        c
-        for c in unicodedata.normalize("NFD", str(value))
-        if unicodedata.category(c) != "Mn"
-    )
-
-
-def friendly_name_slugify(value):
-    value = (
-        strip_accents(value)
-        .lower()
-        .replace(" ", "-")
-        .replace("_", "-")
-        .replace("--", "-")
-        .strip("-")
-    )
-    return "".join(c for c in value if c in ALLOWED_NAME_CHARS)
+def friendly_name_slugify(value: str) -> str:
+    """Convert a friendly name to a slug with dashes instead of underscores."""
+    # First use the standard slugify, then convert underscores to dashes
+    return slugify(value).replace("_", "-")
diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py
index 529a0815b8..480285b6c1 100644
--- a/esphome/dashboard/web_server.py
+++ b/esphome/dashboard/web_server.py
@@ -639,7 +639,11 @@ class DownloadListRequestHandler(BaseHandler):
 
         if platform.upper() in ESP32_VARIANTS:
             platform = "esp32"
-        elif platform in (const.PLATFORM_RTL87XX, const.PLATFORM_BK72XX):
+        elif platform in (
+            const.PLATFORM_RTL87XX,
+            const.PLATFORM_BK72XX,
+            const.PLATFORM_LN882X,
+        ):
             platform = "libretiny"
 
         try:
@@ -837,6 +841,10 @@ class BoardsRequestHandler(BaseHandler):
             from esphome.components.bk72xx.boards import BOARDS as BK72XX_BOARDS
 
             boards = BK72XX_BOARDS
+        elif platform == const.PLATFORM_LN882X:
+            from esphome.components.ln882x.boards import BOARDS as LN882X_BOARDS
+
+            boards = LN882X_BOARDS
         elif platform == const.PLATFORM_RTL87XX:
             from esphome.components.rtl87xx.boards import BOARDS as RTL87XX_BOARDS
 
diff --git a/esphome/dashboard/enum.py b/esphome/enum.py
similarity index 100%
rename from esphome/dashboard/enum.py
rename to esphome/enum.py
diff --git a/esphome/helpers.py b/esphome/helpers.py
index d95546ac94..bf0e3b5cf7 100644
--- a/esphome/helpers.py
+++ b/esphome/helpers.py
@@ -29,6 +29,53 @@ def ensure_unique_string(preferred_string, current_strings):
     return test_string
 
 
+def fnv1a_32bit_hash(string: str) -> int:
+    """FNV-1a 32-bit hash function.
+
+    Note: This uses 32-bit hash instead of 64-bit for several reasons:
+    1. ESPHome targets 32-bit microcontrollers with limited RAM (often <320KB)
+    2. Using 64-bit hashes would double the RAM usage for storing IDs
+    3. 64-bit operations are slower on 32-bit processors
+
+    While there's a ~50% collision probability at ~77,000 unique IDs,
+    ESPHome validates for collisions at compile time, preventing any
+    runtime issues. In practice, most ESPHome installations only have
+    a handful of area_ids and device_ids (typically <10 areas and <100
+    devices), making collisions virtually impossible.
+    """
+    hash_value = 2166136261
+    for char in string:
+        hash_value ^= ord(char)
+        hash_value = (hash_value * 16777619) & 0xFFFFFFFF
+    return hash_value
+
+
+def strip_accents(value: str) -> str:
+    """Remove accents from a string."""
+    import unicodedata
+
+    return "".join(
+        c
+        for c in unicodedata.normalize("NFD", str(value))
+        if unicodedata.category(c) != "Mn"
+    )
+
+
+def slugify(value: str) -> str:
+    """Convert a string to a valid C++ identifier slug."""
+    from esphome.const import ALLOWED_NAME_CHARS
+
+    value = (
+        strip_accents(value)
+        .lower()
+        .replace(" ", "_")
+        .replace("-", "_")
+        .replace("__", "_")
+        .strip("_")
+    )
+    return "".join(c for c in value if c in ALLOWED_NAME_CHARS)
+
+
 def indent_all_but_first_and_last(text, padding="  "):
     lines = text.splitlines(True)
     if len(lines) <= 2:
diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml
index 8460de5638..c43b622684 100644
--- a/esphome/idf_component.yml
+++ b/esphome/idf_component.yml
@@ -1,13 +1,21 @@
 dependencies:
-  esp-tflite-micro:
-    git: https://github.com/espressif/esp-tflite-micro.git
-    version: v1.3.1
-  esp32_camera:
-    git: https://github.com/espressif/esp32-camera.git
-    version: v2.0.15
-  mdns:
-    git: https://github.com/espressif/esp-protocols.git
-    version: mdns-v1.8.2
-    path: components/mdns
+  espressif/esp-tflite-micro:
+    version: 1.3.3~1
+  espressif/esp32-camera:
+    version: 2.0.15
+  espressif/mdns:
+    version: 1.8.2
+  espressif/esp_wifi_remote:
+    version: 0.10.2
     rules:
-      - if: "idf_version >=5.0"
+      - if: "target in [esp32h2, esp32p4]"
+  espressif/eppp_link:
+    version: 0.2.0
+    rules:
+      - if: "target in [esp32h2, esp32p4]"
+  espressif/esp_hosted:
+    version: 2.0.11
+    rules:
+      - if: "target in [esp32h2, esp32p4]"
+  zorxx/multipart-parser:
+    version: 1.0.1
diff --git a/esphome/loader.py b/esphome/loader.py
index 79a1d7f576..7b2472521a 100644
--- a/esphome/loader.py
+++ b/esphome/loader.py
@@ -112,8 +112,17 @@ class ComponentManifest:
         This will return all cpp source files that are located in the same folder as the
         loaded .py file (does not look through subdirectories)
         """
-        ret = []
+        ret: list[FileResource] = []
 
+        # Get filter function for source files
+        filter_source_files_func = getattr(self.module, "FILTER_SOURCE_FILES", None)
+
+        # Get list of files to exclude
+        excluded_files = (
+            set(filter_source_files_func()) if filter_source_files_func else set()
+        )
+
+        # Process all resources
         for resource in (
             r.name
             for r in importlib.resources.files(self.package).iterdir()
@@ -124,6 +133,11 @@ class ComponentManifest:
             if not importlib.resources.files(self.package).joinpath(resource).is_file():
                 # Not a resource = this is a directory (yeah this is confusing)
                 continue
+
+            # Skip excluded files
+            if resource in excluded_files:
+                continue
+
             ret.append(FileResource(self.package, resource))
         return ret
 
diff --git a/esphome/pins.py b/esphome/pins.py
index 724cd25d82..4f9b4859a1 100644
--- a/esphome/pins.py
+++ b/esphome/pins.py
@@ -1,5 +1,8 @@
+from collections.abc import Callable
 from functools import reduce
+from logging import Logger
 import operator
+from typing import Any
 
 import esphome.config_validation as cv
 from esphome.const import (
@@ -15,6 +18,7 @@ from esphome.const import (
     CONF_PULLUP,
 )
 from esphome.core import CORE
+from esphome.cpp_generator import MockObjClass
 
 
 class PinRegistry(dict):
@@ -216,7 +220,9 @@ def gpio_flags_expr(mode):
 
 
 gpio_pin_schema = _schema_creator
-internal_gpio_pin_number = _internal_number_creator
+internal_gpio_pin_number = _internal_number_creator(
+    {CONF_OUTPUT: True, CONF_INPUT: True}
+)
 gpio_output_pin_schema = _schema_creator(
     {
         CONF_OUTPUT: True,
@@ -262,7 +268,7 @@ internal_gpio_input_pullup_pin_number = _internal_number_creator(
 )
 
 
-def check_strapping_pin(conf, strapping_pin_list, logger):
+def check_strapping_pin(conf, strapping_pin_list: set[int], logger: Logger):
     num = conf[CONF_NUMBER]
     if num in strapping_pin_list and not conf.get(CONF_IGNORE_STRAPPING_WARNING):
         logger.warning(
@@ -291,11 +297,11 @@ def gpio_validate_modes(value):
 
 
 def gpio_base_schema(
-    pin_type,
-    number_validator,
+    pin_type: MockObjClass,
+    number_validator: Callable[[Any], Any],
     modes=GPIO_STANDARD_MODES,
-    mode_validator=gpio_validate_modes,
-    invertable=True,
+    mode_validator: Callable[[Any], Any] = gpio_validate_modes,
+    invertible: bool = True,
 ):
     """
     Generate a base gpio pin schema
@@ -303,7 +309,7 @@ def gpio_base_schema(
     :param number_validator: A validator for the pin number
     :param modes: The available modes, default is all standard modes
     :param mode_validator: A validator function for the pin mode
-    :param invertable: If the pin supports hardware inversion
+    :param invertible: If the pin supports hardware inversion
     :return: A schema for the pin
     """
     mode_default = len(modes) == 1
@@ -328,7 +334,7 @@ def gpio_base_schema(
         }
     )
 
-    if invertable:
+    if invertible:
         return schema.extend({cv.Optional(CONF_INVERTED, default=False): cv.boolean})
 
     return schema
diff --git a/esphome/wizard.py b/esphome/wizard.py
index 7b4d87be63..1826487aa4 100644
--- a/esphome/wizard.py
+++ b/esphome/wizard.py
@@ -83,6 +83,11 @@ bk72xx:
   board: {board}
 """
 
+LN882X_CONFIG = """
+ln882x:
+  board: {board}
+"""
+
 RTL87XX_CONFIG = """
 rtl87xx:
   board: {board}
@@ -93,6 +98,7 @@ HARDWARE_BASE_CONFIGS = {
     "ESP32": ESP32_CONFIG,
     "RP2040": RP2040_CONFIG,
     "BK72XX": BK72XX_CONFIG,
+    "LN882X": LN882X_CONFIG,
     "RTL87XX": RTL87XX_CONFIG,
 }
 
@@ -157,7 +163,7 @@ def wizard_file(**kwargs):
 """
 
     # pylint: disable=consider-using-f-string
-    if kwargs["platform"] in ["ESP8266", "ESP32", "BK72XX", "RTL87XX"]:
+    if kwargs["platform"] in ["ESP8266", "ESP32", "BK72XX", "LN882X", "RTL87XX"]:
         config += """
   # Enable fallback hotspot (captive portal) in case wifi connection fails
   ap:
@@ -181,6 +187,7 @@ def wizard_write(path, **kwargs):
     from esphome.components.bk72xx import boards as bk72xx_boards
     from esphome.components.esp32 import boards as esp32_boards
     from esphome.components.esp8266 import boards as esp8266_boards
+    from esphome.components.ln882x import boards as ln882x_boards
     from esphome.components.rp2040 import boards as rp2040_boards
     from esphome.components.rtl87xx import boards as rtl87xx_boards
 
@@ -200,6 +207,8 @@ def wizard_write(path, **kwargs):
             platform = "RP2040"
         elif board in bk72xx_boards.BOARDS:
             platform = "BK72XX"
+        elif board in ln882x_boards.BOARDS:
+            platform = "LN882X"
         elif board in rtl87xx_boards.BOARDS:
             platform = "RTL87XX"
         else:
@@ -253,6 +262,7 @@ def wizard(path):
     from esphome.components.bk72xx import boards as bk72xx_boards
     from esphome.components.esp32 import boards as esp32_boards
     from esphome.components.esp8266 import boards as esp8266_boards
+    from esphome.components.ln882x import boards as ln882x_boards
     from esphome.components.rp2040 import boards as rp2040_boards
     from esphome.components.rtl87xx import boards as rtl87xx_boards
 
@@ -325,7 +335,7 @@ def wizard(path):
         "firmwares for it."
     )
 
-    wizard_platforms = ["ESP32", "ESP8266", "BK72XX", "RTL87XX", "RP2040"]
+    wizard_platforms = ["ESP32", "ESP8266", "BK72XX", "LN882X", "RTL87XX", "RP2040"]
     safe_print(
         "Please choose one of the supported microcontrollers "
         "(Use ESP8266 for Sonoff devices)."
@@ -361,7 +371,7 @@ def wizard(path):
         board_link = (
             "https://www.raspberrypi.com/documentation/microcontrollers/rp2040.html"
         )
-    elif platform in ["BK72XX", "RTL87XX"]:
+    elif platform in ["BK72XX", "LN882X", "RTL87XX"]:
         board_link = "https://docs.libretiny.eu/docs/status/supported/"
     else:
         raise NotImplementedError("Unknown platform!")
@@ -384,6 +394,9 @@ def wizard(path):
     elif platform == "BK72XX":
         safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "cb2s")}".')
         boards_list = bk72xx_boards.BOARDS.items()
+    elif platform == "LN882X":
+        safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "wl2s")}".')
+        boards_list = ln882x_boards.BOARDS.items()
     elif platform == "RTL87XX":
         safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "wr3")}".')
         boards_list = rtl87xx_boards.BOARDS.items()
diff --git a/esphome/writer.py b/esphome/writer.py
index a47112e1fd..943dfa78cc 100644
--- a/esphome/writer.py
+++ b/esphome/writer.py
@@ -107,6 +107,11 @@ def storage_should_clean(old: StorageJSON, new: StorageJSON) -> bool:
         return True
     if old.build_path != new.build_path:
         return True
+
+    return False
+
+
+def storage_should_update_cmake_cache(old: StorageJSON, new: StorageJSON) -> bool:
     if (
         old.loaded_integrations != new.loaded_integrations
         or old.loaded_platforms != new.loaded_platforms
@@ -126,10 +131,11 @@ def update_storage_json():
         return
 
     if storage_should_clean(old, new):
-        _LOGGER.info(
-            "Core config, version or integrations changed, cleaning build files..."
-        )
+        _LOGGER.info("Core config, version changed, cleaning build files...")
         clean_build()
+    elif storage_should_update_cmake_cache(old, new):
+        _LOGGER.info("Integrations changed, cleaning cmake cache...")
+        clean_cmake_cache()
 
     new.save(path)
 
@@ -153,6 +159,9 @@ def get_ini_content():
     # Sort to avoid changing build flags order
     CORE.add_platformio_option("build_flags", sorted(CORE.build_flags))
 
+    # Sort to avoid changing build unflags order
+    CORE.add_platformio_option("build_unflags", sorted(CORE.build_unflags))
+
     content = "[platformio]\n"
     content += f"description = ESPHome {__version__}\n"
 
@@ -350,6 +359,15 @@ def write_cpp(code_s):
     write_file_if_changed(path, full_file)
 
 
+def clean_cmake_cache():
+    pioenvs = CORE.relative_pioenvs_path()
+    if os.path.isdir(pioenvs):
+        pioenvs_cmake_path = CORE.relative_pioenvs_path(CORE.name, "CMakeCache.txt")
+        if os.path.isfile(pioenvs_cmake_path):
+            _LOGGER.info("Deleting %s", pioenvs_cmake_path)
+            os.remove(pioenvs_cmake_path)
+
+
 def clean_build():
     import shutil
 
diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py
index bd1806affc..e52fc9e788 100644
--- a/esphome/yaml_util.py
+++ b/esphome/yaml_util.py
@@ -292,8 +292,6 @@ class ESPHomeLoaderMixin:
             if file is None:
                 raise yaml.MarkedYAMLError("Must include 'file'", node.start_mark)
             vars = fields.get(CONF_VARS)
-            if vars:
-                vars = {k: str(v) for k, v in vars.items()}
             return file, vars
 
         if isinstance(node, yaml.nodes.MappingNode):
diff --git a/platformio.ini b/platformio.ini
index 27da883ab3..0d67e23222 100644
--- a/platformio.ini
+++ b/platformio.ini
@@ -4,7 +4,7 @@
 ; It's *not* used during runtime.
 
 [platformio]
-default_envs = esp8266-arduino, esp32-arduino, esp32-idf, bk72xx-arduino
+default_envs = esp8266-arduino, esp32-arduino, esp32-idf, bk72xx-arduino, ln882h-arduino
 ; Ideally, we want src_dir to be the root directory of the repository, to mimic the runtime build
 ; environment as best as possible. Unfortunately, the ESP-IDF toolchain really doesn't like this
 ; being the root directory. Instead, set esphome/ as the source directory, all our sources are in
@@ -33,8 +33,7 @@ build_flags =
 ; This are common settings for all environments.
 [common]
 lib_deps =
-    esphome/noise-c@0.1.4                  ; api
-    makuna/NeoPixelBus@2.7.3               ; neopixelbus
+    esphome/noise-c@0.1.10                  ; api
     improv/Improv@1.2.4                    ; improv_serial / esp32_improv
     bblanchon/ArduinoJson@6.18.5           ; json
     wjtje/qr-code-generator-library@1.7.0  ; qr_code
@@ -48,6 +47,15 @@ lib_deps =
     lvgl/lvgl@8.4.0                                       ; lvgl
 build_flags =
     -DESPHOME_LOG_LEVEL=ESPHOME_LOG_LEVEL_VERY_VERBOSE
+    -std=gnu++20
+build_unflags =
+    -std=gnu++11
+    -std=gnu++14
+    -std=gnu++17
+    -std=gnu++23
+    -std=gnu++2a
+    -std=gnu++2b
+    -std=gnu++2c
 src_filter =
     +<./>
     +<../tests/dummy_main.cpp>
@@ -62,17 +70,19 @@ lib_deps =
     SPI                                                   ; spi (Arduino built-in)
     Wire                                                  ; i2c (Arduino built-int)
     heman/AsyncMqttClient-esphome@1.0.0                   ; mqtt
-    esphome/ESPAsyncWebServer-esphome@3.3.0               ; web_server_base
+    ESP32Async/ESPAsyncWebServer@3.7.8                    ; web_server_base
     fastled/FastLED@3.9.16                                ; fastled_base
     mikalhart/TinyGPSPlus@1.1.0                           ; gps
     freekode/TM1651@1.0.1                                 ; tm1651
     glmnet/Dsmr@0.7                                       ; dsmr
     rweather/Crypto@0.4.0                                 ; dsmr
     dudanov/MideaUART@1.1.9                               ; midea
-    tonia/HeatpumpIR@1.0.32                               ; heatpumpir
+    tonia/HeatpumpIR@1.0.35                               ; heatpumpir
 build_flags =
     ${common.build_flags}
     -DUSE_ARDUINO
+build_unflags =
+    ${common.build_unflags}
 
 ; This are common settings for all IDF-framework based environments.
 [common:idf]
@@ -80,6 +90,8 @@ extends = common
 build_flags =
     ${common.build_flags}
     -DUSE_ESP_IDF
+build_unflags =
+    ${common.build_unflags}
 
 ; This are common settings for the ESP8266 using Arduino.
 [common:esp8266-arduino]
@@ -93,7 +105,8 @@ lib_deps =
     ${common:arduino.lib_deps}
     ESP8266WiFi                           ; wifi (Arduino built-in)
     Update                                ; ota (Arduino built-in)
-    esphome/ESPAsyncTCP-esphome@2.0.0     ; async_tcp
+    ESP32Async/ESPAsyncTCP@2.0.0          ; async_tcp
+    makuna/NeoPixelBus@2.7.3              ; neopixelbus
     ESP8266HTTPClient                     ; http_request (Arduino built-in)
     ESP8266mDNS                           ; mdns (Arduino built-in)
     DNSServer                             ; captive_portal (Arduino built-in)
@@ -104,29 +117,34 @@ build_flags =
     -Wno-nonnull-compare
     -DUSE_ESP8266
     -DUSE_ESP8266_FRAMEWORK_ARDUINO
+build_unflags =
+    ${common.build_unflags}
 extra_scripts = post:esphome/components/esp8266/post_build.py.script
 
 ; This are common settings for the ESP32 (all variants) using Arduino.
 [common:esp32-arduino]
 extends = common:arduino
-platform = platformio/espressif32@5.4.0
+platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.13/platform-espressif32.zip
 platform_packages =
-    platformio/framework-arduinoespressif32@~3.20005.0
+    pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.1.3/esp32-3.1.3.zip
 
 framework = arduino
 lib_deps =
     ; order matters with lib-deps; some of the libs in common:arduino.lib_deps
     ; don't declare built-in libraries as dependencies, so they have to be declared first
     FS                                   ; web_server_base (Arduino built-in)
+    Networking                           ; wifi,web_server_base,ethernet (Arduino built-in)
     WiFi                                 ; wifi,web_server_base,ethernet (Arduino built-in)
     Update                               ; ota,web_server_base (Arduino built-in)
     ${common:arduino.lib_deps}
-    esphome/AsyncTCP-esphome@2.1.4       ; async_tcp
-    WiFiClientSecure                     ; http_request,nextion (Arduino built-in)
+    ESP32Async/AsyncTCP@3.4.4            ; async_tcp
+    NetworkClientSecure                  ; http_request,nextion (Arduino built-in)
     HTTPClient                           ; http_request,nextion (Arduino built-in)
     ESPmDNS                              ; mdns (Arduino built-in)
+    ESP32 Async UDP                      ; captive_portal (Arduino built-in)
     DNSServer                            ; captive_portal (Arduino built-in)
-    esphome/ESP32-audioI2S@2.2.0         ; i2s_audio
+    makuna/NeoPixelBus@2.8.0             ; neopixelbus
+    esphome/ESP32-audioI2S@2.3.0         ; i2s_audio
     droscy/esp_wireguard@0.4.2           ; wireguard
     esphome/esp-audio-libs@1.1.4         ; audio
 
@@ -135,6 +153,8 @@ build_flags =
     -DUSE_ESP32
     -DUSE_ESP32_FRAMEWORK_ARDUINO
     -DAUDIO_NO_SD_FS                ; i2s_audio
+build_unflags =
+    ${common.build_unflags}
 extra_scripts = post:esphome/components/esp32/post_build.py.script
 
 ; This are common settings for the ESP32 (all variants) using IDF.
@@ -155,6 +175,8 @@ build_flags =
     -Wno-nonnull-compare
     -DUSE_ESP32
     -DUSE_ESP32_FRAMEWORK_ESP_IDF
+build_unflags =
+    ${common.build_unflags}
 extra_scripts = post:esphome/components/esp32/post_build.py.script
 
 ; This are common settings for the ESP32 using the latest ESP-IDF version.
@@ -181,17 +203,21 @@ build_flags =
     ${common:arduino.build_flags}
     -DUSE_RP2040
     -DUSE_RP2040_FRAMEWORK_ARDUINO
+build_unflags =
+    ${common.build_unflags}
 
 ; This are common settings for the LibreTiny (all variants) using Arduino.
 [common:libretiny-arduino]
 extends = common:arduino
-platform = libretiny
+platform = libretiny@1.9.1
 framework = arduino
 lib_deps =
     droscy/esp_wireguard@0.4.2    ; wireguard
 build_flags =
     ${common:arduino.build_flags}
     -DUSE_LIBRETINY
+build_unflags =
+    ${common.build_unflags}
 build_src_flags = -include Arduino.h
 
 ; This is the common settings for the nRF52 using Zephyr.
@@ -224,6 +250,8 @@ board = nodemcuv2
 build_flags =
     ${common:esp8266-arduino.build_flags}
     ${flags:runtime.build_flags}
+build_unflags =
+    ${common.build_unflags}
 
 [env:esp8266-arduino-tidy]
 extends = common:esp8266-arduino
@@ -231,6 +259,8 @@ board = nodemcuv2
 build_flags =
     ${common:esp8266-arduino.build_flags}
     ${flags:clangtidy.build_flags}
+build_unflags =
+    ${common.build_unflags}
 
 ;;;;;;;; ESP32 ;;;;;;;;
 
@@ -242,6 +272,8 @@ build_flags =
     ${common:esp32-arduino.build_flags}
     ${flags:runtime.build_flags}
     -DUSE_ESP32_VARIANT_ESP32
+build_unflags =
+    ${common.build_unflags}
 
 [env:esp32-arduino-tidy]
 extends = common:esp32-arduino
@@ -250,6 +282,8 @@ build_flags =
     ${common:esp32-arduino.build_flags}
     ${flags:clangtidy.build_flags}
     -DUSE_ESP32_VARIANT_ESP32
+build_unflags =
+    ${common.build_unflags}
 
 [env:esp32-idf]
 extends = common:esp32-idf
@@ -259,6 +293,8 @@ build_flags =
     ${common:esp32-idf.build_flags}
     ${flags:runtime.build_flags}
     -DUSE_ESP32_VARIANT_ESP32
+build_unflags =
+    ${common.build_unflags}
 
 [env:esp32-idf-5_3]
 extends = common:esp32-idf-5_3
@@ -268,6 +304,8 @@ build_flags =
     ${common:esp32-idf.build_flags}
     ${flags:runtime.build_flags}
     -DUSE_ESP32_VARIANT_ESP32
+build_unflags =
+    ${common.build_unflags}
 
 [env:esp32-idf-tidy]
 extends = common:esp32-idf
@@ -277,6 +315,8 @@ build_flags =
     ${common:esp32-idf.build_flags}
     ${flags:clangtidy.build_flags}
     -DUSE_ESP32_VARIANT_ESP32
+build_unflags =
+    ${common.build_unflags}
 
 ;;;;;;;; ESP32-C3 ;;;;;;;;
 
@@ -287,6 +327,8 @@ build_flags =
     ${common:esp32-arduino.build_flags}
     ${flags:runtime.build_flags}
     -DUSE_ESP32_VARIANT_ESP32C3
+build_unflags =
+    ${common.build_unflags}
 
 [env:esp32c3-arduino-tidy]
 extends = common:esp32-arduino
@@ -295,6 +337,8 @@ build_flags =
     ${common:esp32-arduino.build_flags}
     ${flags:clangtidy.build_flags}
     -DUSE_ESP32_VARIANT_ESP32C3
+build_unflags =
+    ${common.build_unflags}
 
 [env:esp32c3-idf]
 extends = common:esp32-idf
@@ -304,6 +348,8 @@ build_flags =
     ${common:esp32-idf.build_flags}
     ${flags:runtime.build_flags}
     -DUSE_ESP32_VARIANT_ESP32C3
+build_unflags =
+    ${common.build_unflags}
 
 [env:esp32c3-idf-5_3]
 extends = common:esp32-idf-5_3
@@ -313,6 +359,8 @@ build_flags =
     ${common:esp32-idf.build_flags}
     ${flags:runtime.build_flags}
     -DUSE_ESP32_VARIANT_ESP32C3
+build_unflags =
+    ${common.build_unflags}
 
 [env:esp32c3-idf-tidy]
 extends = common:esp32-idf
@@ -322,6 +370,8 @@ build_flags =
     ${common:esp32-idf.build_flags}
     ${flags:clangtidy.build_flags}
     -DUSE_ESP32_VARIANT_ESP32C3
+build_unflags =
+    ${common.build_unflags}
 
 ;;;;;;;; ESP32-C6 ;;;;;;;;
 
@@ -343,6 +393,8 @@ build_flags =
     ${common:esp32-arduino.build_flags}
     ${flags:runtime.build_flags}
     -DUSE_ESP32_VARIANT_ESP32S2
+build_unflags =
+    ${common.build_unflags}
 
 [env:esp32s2-arduino-tidy]
 extends = common:esp32-arduino
@@ -351,6 +403,8 @@ build_flags =
     ${common:esp32-arduino.build_flags}
     ${flags:clangtidy.build_flags}
     -DUSE_ESP32_VARIANT_ESP32S2
+build_unflags =
+    ${common.build_unflags}
 
 [env:esp32s2-idf]
 extends = common:esp32-idf
@@ -360,6 +414,8 @@ build_flags =
     ${common:esp32-idf.build_flags}
     ${flags:runtime.build_flags}
     -DUSE_ESP32_VARIANT_ESP32S2
+build_unflags =
+    ${common.build_unflags}
 
 [env:esp32s2-idf-5_3]
 extends = common:esp32-idf-5_3
@@ -369,6 +425,8 @@ build_flags =
     ${common:esp32-idf.build_flags}
     ${flags:runtime.build_flags}
     -DUSE_ESP32_VARIANT_ESP32S2
+build_unflags =
+    ${common.build_unflags}
 
 [env:esp32s2-idf-tidy]
 extends = common:esp32-idf
@@ -378,6 +436,8 @@ build_flags =
     ${common:esp32-idf.build_flags}
     ${flags:clangtidy.build_flags}
     -DUSE_ESP32_VARIANT_ESP32S2
+build_unflags =
+    ${common.build_unflags}
 
 ;;;;;;;; ESP32-S3 ;;;;;;;;
 
@@ -388,6 +448,8 @@ build_flags =
     ${common:esp32-arduino.build_flags}
     ${flags:runtime.build_flags}
     -DUSE_ESP32_VARIANT_ESP32S3
+build_unflags =
+    ${common.build_unflags}
 
 [env:esp32s3-arduino-tidy]
 extends = common:esp32-arduino
@@ -396,6 +458,8 @@ build_flags =
     ${common:esp32-arduino.build_flags}
     ${flags:clangtidy.build_flags}
     -DUSE_ESP32_VARIANT_ESP32S3
+build_unflags =
+    ${common.build_unflags}
 
 [env:esp32s3-idf]
 extends = common:esp32-idf
@@ -405,6 +469,8 @@ build_flags =
     ${common:esp32-idf.build_flags}
     ${flags:runtime.build_flags}
     -DUSE_ESP32_VARIANT_ESP32S3
+build_unflags =
+    ${common.build_unflags}
 
 [env:esp32s3-idf-5_3]
 extends = common:esp32-idf-5_3
@@ -414,6 +480,8 @@ build_flags =
     ${common:esp32-idf.build_flags}
     ${flags:runtime.build_flags}
     -DUSE_ESP32_VARIANT_ESP32S3
+build_unflags =
+    ${common.build_unflags}
 
 [env:esp32s3-idf-tidy]
 extends = common:esp32-idf
@@ -423,6 +491,8 @@ build_flags =
     ${common:esp32-idf.build_flags}
     ${flags:clangtidy.build_flags}
     -DUSE_ESP32_VARIANT_ESP32S3
+build_unflags =
+    ${common.build_unflags}
 
 ;;;;;;;; ESP32-P4 ;;;;;;;;
 
@@ -444,6 +514,8 @@ board = rpipico
 build_flags =
     ${common:rp2040-arduino.build_flags}
     ${flags:runtime.build_flags}
+build_unflags =
+    ${common.build_unflags}
 
 ;;;;;;;; LibreTiny ;;;;;;;;
 
@@ -455,6 +527,19 @@ build_flags =
     ${flags:runtime.build_flags}
     -DUSE_BK72XX
     -DUSE_LIBRETINY_VARIANT_BK7231N
+build_unflags =
+    ${common.build_unflags}
+
+[env:ln882h-arduino]
+extends = common:libretiny-arduino
+board = generic-ln882hki
+build_flags =
+    ${common:libretiny-arduino.build_flags}
+    ${flags:runtime.build_flags}
+    -DUSE_LN882X
+    -DUSE_LIBRETINY_VARIANT_LN882H
+build_unflags =
+    ${common.build_unflags}
 
 [env:rtl87xxb-arduino]
 extends = common:libretiny-arduino
@@ -464,6 +549,8 @@ build_flags =
     ${flags:runtime.build_flags}
     -DUSE_RTL87XX
     -DUSE_LIBRETINY_VARIANT_RTL8710B
+build_unflags =
+    ${common.build_unflags}
 
 [env:rtl87xxc-arduino]
 extends = common:libretiny-arduino
@@ -473,16 +560,20 @@ build_flags =
     ${flags:runtime.build_flags}
     -DUSE_RTL87XX
     -DUSE_LIBRETINY_VARIANT_RTL8720C
+build_unflags =
+    ${common.build_unflags}
 
 [env:host]
 extends = common
 platform = platformio/native
 lib_deps =
-    esphome/noise-c@0.1.1  ; used by api
+    esphome/noise-c@0.1.10  ; used by api
 build_flags =
     ${common.build_flags}
     -DUSE_HOST
-    -std=c++17
+    -std=c++20
+build_unflags =
+    ${common.build_unflags}
 
 ;;;;;;;; nRF52 ;;;;;;;;
 
@@ -492,6 +583,8 @@ board = adafruit_feather_nrf52840
 build_flags =
     ${common:nrf52-zephyr.build_flags}
     ${flags:runtime.build_flags}
+build_unflags =
+    ${common.build_unflags}
 
 [env:nrf52-tidy]
 extends = common:nrf52-zephyr
@@ -499,3 +592,5 @@ board = adafruit_feather_nrf52840
 build_flags =
     ${common:nrf52-zephyr.build_flags}
     ${flags:clangtidy.build_flags}
+build_unflags =
+    ${common.build_unflags}
diff --git a/pyproject.toml b/pyproject.toml
index 3bec607150..97b0df9eff 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -120,10 +120,12 @@ select = [
 
 ignore = [
   "E501", # line too long
+  "PLC0415", # `import` should be at the top-level of a file
   "PLR0911", # Too many return statements ({returns} > {max_returns})
   "PLR0912", # Too many branches ({branches} > {max_branches})
   "PLR0913", # Too many arguments to function call ({c_args} > {max_args})
   "PLR0915", # Too many statements ({statements} > {max_statements})
+  "PLW1641", # Object does not implement `__hash__` method
   "PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable
   "PLW2901", # Outer {outer_kind} variable {name} overwritten by inner {inner_kind} target
   "UP038", # https://github.com/astral-sh/ruff/issues/7871 https://github.com/astral-sh/ruff/pull/16681
diff --git a/requirements.txt b/requirements.txt
index 682f9dbe60..d056f22e28 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -10,17 +10,18 @@ tzlocal==5.3.1    # from time
 tzdata>=2021.1  # from time
 pyserial==3.5
 platformio==6.1.18  # When updating platformio, also update /docker/Dockerfile
-esptool==4.8.1
+esptool==4.9.0
 click==8.1.7
 esphome-dashboard==20250514.0
-aioesphomeapi==32.2.3
+aioesphomeapi==34.2.0
 zeroconf==0.147.0
-puremagic==1.29
+puremagic==1.30
 ruamel.yaml==0.18.14 # dashboard_import
 esphome-glyphsets==0.2.0
 pillow==10.4.0
 cairosvg==2.8.2
 freetype-py==2.5.1
+jinja2==3.1.6
 
 # esp-idf requires this, but doesn't bundle it by default
 # https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24
diff --git a/requirements_test.txt b/requirements_test.txt
index 689cd9e75e..ef1fc4f2d6 100644
--- a/requirements_test.txt
+++ b/requirements_test.txt
@@ -1,14 +1,14 @@
 pylint==3.3.7
-flake8==7.2.0  # also change in .pre-commit-config.yaml when updating
-ruff==0.11.13  # 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
 pyupgrade==3.20.0  # also change in .pre-commit-config.yaml when updating
 pre-commit
 
 # Unit tests
-pytest==8.4.0
-pytest-cov==6.1.1
+pytest==8.4.1
+pytest-cov==6.2.1
 pytest-mock==3.14.1
-pytest-asyncio==0.26.0
+pytest-asyncio==1.0.0
 pytest-xdist==3.7.0
 asyncmock==0.4.2
 hypothesis==6.92.1
diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py
index 24b6bef843..df1f3f8caa 100755
--- a/script/api_protobuf/api_protobuf.py
+++ b/script/api_protobuf/api_protobuf.py
@@ -290,7 +290,7 @@ class DoubleType(TypeInfo):
     wire_type = WireType.FIXED64  # Uses wire type 1 according to protobuf spec
 
     def dump(self, name: str) -> str:
-        o = f'sprintf(buffer, "%g", {name});\n'
+        o = f'snprintf(buffer, sizeof(buffer), "%g", {name});\n'
         o += "out.append(buffer);"
         return o
 
@@ -312,7 +312,7 @@ class FloatType(TypeInfo):
     wire_type = WireType.FIXED32  # Uses wire type 5
 
     def dump(self, name: str) -> str:
-        o = f'sprintf(buffer, "%g", {name});\n'
+        o = f'snprintf(buffer, sizeof(buffer), "%g", {name});\n'
         o += "out.append(buffer);"
         return o
 
@@ -334,7 +334,7 @@ class Int64Type(TypeInfo):
     wire_type = WireType.VARINT  # Uses wire type 0
 
     def dump(self, name: str) -> str:
-        o = f'sprintf(buffer, "%lld", {name});\n'
+        o = f'snprintf(buffer, sizeof(buffer), "%lld", {name});\n'
         o += "out.append(buffer);"
         return o
 
@@ -356,7 +356,7 @@ class UInt64Type(TypeInfo):
     wire_type = WireType.VARINT  # Uses wire type 0
 
     def dump(self, name: str) -> str:
-        o = f'sprintf(buffer, "%llu", {name});\n'
+        o = f'snprintf(buffer, sizeof(buffer), "%llu", {name});\n'
         o += "out.append(buffer);"
         return o
 
@@ -378,7 +378,7 @@ class Int32Type(TypeInfo):
     wire_type = WireType.VARINT  # Uses wire type 0
 
     def dump(self, name: str) -> str:
-        o = f'sprintf(buffer, "%" PRId32, {name});\n'
+        o = f'snprintf(buffer, sizeof(buffer), "%" PRId32, {name});\n'
         o += "out.append(buffer);"
         return o
 
@@ -400,7 +400,7 @@ class Fixed64Type(TypeInfo):
     wire_type = WireType.FIXED64  # Uses wire type 1
 
     def dump(self, name: str) -> str:
-        o = f'sprintf(buffer, "%llu", {name});\n'
+        o = f'snprintf(buffer, sizeof(buffer), "%llu", {name});\n'
         o += "out.append(buffer);"
         return o
 
@@ -422,7 +422,7 @@ class Fixed32Type(TypeInfo):
     wire_type = WireType.FIXED32  # Uses wire type 5
 
     def dump(self, name: str) -> str:
-        o = f'sprintf(buffer, "%" PRIu32, {name});\n'
+        o = f'snprintf(buffer, sizeof(buffer), "%" PRIu32, {name});\n'
         o += "out.append(buffer);"
         return o
 
@@ -526,11 +526,15 @@ class BytesType(TypeInfo):
     reference_type = "std::string &"
     const_reference_type = "const std::string &"
     decode_length = "value.as_string()"
-    encode_func = "encode_string"
+    encode_func = "encode_bytes"
     wire_type = WireType.LENGTH_DELIMITED  # Uses wire type 2
 
+    @property
+    def encode_content(self) -> str:
+        return f"buffer.encode_bytes({self.number}, reinterpret_cast(this->{self.field_name}.data()), this->{self.field_name}.size());"
+
     def dump(self, name: str) -> str:
-        o = f'out.append("\'").append({name}).append("\'");'
+        o = f"out.append(format_hex_pretty({name}));"
         return o
 
     def get_size_calculation(self, name: str, force: bool = False) -> str:
@@ -551,7 +555,7 @@ class UInt32Type(TypeInfo):
     wire_type = WireType.VARINT  # Uses wire type 0
 
     def dump(self, name: str) -> str:
-        o = f'sprintf(buffer, "%" PRIu32, {name});\n'
+        o = f'snprintf(buffer, sizeof(buffer), "%" PRIu32, {name});\n'
         o += "out.append(buffer);"
         return o
 
@@ -603,7 +607,7 @@ class SFixed32Type(TypeInfo):
     wire_type = WireType.FIXED32  # Uses wire type 5
 
     def dump(self, name: str) -> str:
-        o = f'sprintf(buffer, "%" PRId32, {name});\n'
+        o = f'snprintf(buffer, sizeof(buffer), "%" PRId32, {name});\n'
         o += "out.append(buffer);"
         return o
 
@@ -625,7 +629,7 @@ class SFixed64Type(TypeInfo):
     wire_type = WireType.FIXED64  # Uses wire type 1
 
     def dump(self, name: str) -> str:
-        o = f'sprintf(buffer, "%lld", {name});\n'
+        o = f'snprintf(buffer, sizeof(buffer), "%lld", {name});\n'
         o += "out.append(buffer);"
         return o
 
@@ -647,7 +651,7 @@ class SInt32Type(TypeInfo):
     wire_type = WireType.VARINT  # Uses wire type 0
 
     def dump(self, name: str) -> str:
-        o = f'sprintf(buffer, "%" PRId32, {name});\n'
+        o = f'snprintf(buffer, sizeof(buffer), "%" PRId32, {name});\n'
         o += "out.append(buffer);"
         return o
 
@@ -669,7 +673,7 @@ class SInt64Type(TypeInfo):
     wire_type = WireType.VARINT  # Uses wire type 0
 
     def dump(self, name: str) -> str:
-        o = f'sprintf(buffer, "%lld", {name});\n'
+        o = f'snprintf(buffer, sizeof(buffer), "%lld", {name});\n'
         o += "out.append(buffer);"
         return o
 
@@ -809,27 +813,137 @@ class RepeatedTypeInfo(TypeInfo):
         return underlying_size * 2
 
 
-def build_enum_type(desc) -> tuple[str, str]:
-    """Builds the enum type."""
+def build_type_usage_map(
+    file_desc: descriptor.FileDescriptorProto,
+) -> tuple[dict[str, str | None], dict[str, str | None]]:
+    """Build mappings for both enums and messages to their ifdefs based on usage.
+
+    Returns:
+        tuple: (enum_ifdef_map, message_ifdef_map)
+    """
+    enum_ifdef_map: dict[str, str | None] = {}
+    message_ifdef_map: dict[str, str | None] = {}
+
+    # Build maps of which types are used by which messages
+    enum_usage: dict[
+        str, set[str]
+    ] = {}  # enum_name -> set of message names that use it
+    message_usage: dict[
+        str, set[str]
+    ] = {}  # message_name -> set of message names that use it
+
+    # Build message name to ifdef mapping for quick lookup
+    message_to_ifdef: dict[str, str | None] = {
+        msg.name: get_opt(msg, pb.ifdef) for msg in file_desc.message_type
+    }
+
+    # Analyze field usage
+    for message in file_desc.message_type:
+        for field in message.field:
+            type_name = field.type_name.split(".")[-1] if field.type_name else None
+            if not type_name:
+                continue
+
+            # Track enum usage
+            if field.type == 14:  # TYPE_ENUM
+                enum_usage.setdefault(type_name, set()).add(message.name)
+            # Track message usage
+            elif field.type == 11:  # TYPE_MESSAGE
+                message_usage.setdefault(type_name, set()).add(message.name)
+
+    # Helper to get unique ifdef from a set of messages
+    def get_unique_ifdef(message_names: set[str]) -> str | None:
+        ifdefs: set[str] = {
+            message_to_ifdef[name]
+            for name in message_names
+            if message_to_ifdef.get(name)
+        }
+        return ifdefs.pop() if len(ifdefs) == 1 else None
+
+    # Build enum ifdef map
+    for enum in file_desc.enum_type:
+        if enum.name in enum_usage:
+            enum_ifdef_map[enum.name] = get_unique_ifdef(enum_usage[enum.name])
+        else:
+            enum_ifdef_map[enum.name] = None
+
+    # Build message ifdef map
+    for message in file_desc.message_type:
+        # Explicit ifdef takes precedence
+        explicit_ifdef = message_to_ifdef.get(message.name)
+        if explicit_ifdef:
+            message_ifdef_map[message.name] = explicit_ifdef
+        elif message.name in message_usage:
+            # Inherit ifdef if all parent messages have the same one
+            message_ifdef_map[message.name] = get_unique_ifdef(
+                message_usage[message.name]
+            )
+        else:
+            message_ifdef_map[message.name] = None
+
+    # Second pass: propagate ifdefs recursively
+    # Keep iterating until no more changes are made
+    changed = True
+    iterations = 0
+    while changed and iterations < 10:  # Add safety limit
+        changed = False
+        iterations += 1
+        for message in file_desc.message_type:
+            # Skip if already has an ifdef
+            if message_ifdef_map.get(message.name):
+                continue
+
+            # Check if this message is used by other messages
+            if message.name not in message_usage:
+                continue
+
+            # Get ifdefs from all messages that use this one
+            parent_ifdefs: set[str] = {
+                message_ifdef_map.get(parent)
+                for parent in message_usage[message.name]
+                if message_ifdef_map.get(parent)
+            }
+
+            # If all parents have the same ifdef, inherit it
+            if len(parent_ifdefs) == 1 and None not in parent_ifdefs:
+                message_ifdef_map[message.name] = parent_ifdefs.pop()
+                changed = True
+
+    return enum_ifdef_map, message_ifdef_map
+
+
+def build_enum_type(desc, enum_ifdef_map) -> tuple[str, str, str]:
+    """Builds the enum type.
+
+    Args:
+        desc: The enum descriptor
+        enum_ifdef_map: Mapping of enum names to their ifdefs
+
+    Returns:
+        tuple: (header_content, cpp_content, dump_cpp_content)
+    """
     name = desc.name
+
     out = f"enum {name} : uint32_t {{\n"
     for v in desc.value:
         out += f"  {v.name} = {v.number},\n"
     out += "};\n"
 
-    cpp = "#ifdef HAS_PROTO_MESSAGE_DUMP\n"
-    cpp += f"template<> const char *proto_enum_to_string(enums::{name} value) {{\n"
-    cpp += "  switch (value) {\n"
-    for v in desc.value:
-        cpp += f"    case enums::{v.name}:\n"
-        cpp += f'      return "{v.name}";\n'
-    cpp += "    default:\n"
-    cpp += '      return "UNKNOWN";\n'
-    cpp += "  }\n"
-    cpp += "}\n"
-    cpp += "#endif\n"
+    # Regular cpp file has no enum content anymore
+    cpp = ""
 
-    return out, cpp
+    # Dump cpp content for enum string conversion
+    dump_cpp = f"template<> const char *proto_enum_to_string(enums::{name} value) {{\n"
+    dump_cpp += "  switch (value) {\n"
+    for v in desc.value:
+        dump_cpp += f"    case enums::{v.name}:\n"
+        dump_cpp += f'      return "{v.name}";\n'
+    dump_cpp += "    default:\n"
+    dump_cpp += '      return "UNKNOWN";\n'
+    dump_cpp += "  }\n"
+    dump_cpp += "}\n"
+
+    return out, cpp, dump_cpp
 
 
 def calculate_message_estimated_size(desc: descriptor.DescriptorProto) -> int:
@@ -851,7 +965,7 @@ def calculate_message_estimated_size(desc: descriptor.DescriptorProto) -> int:
 def build_message_type(
     desc: descriptor.DescriptorProto,
     base_class_fields: dict[str, list[descriptor.FieldDescriptorProto]] = None,
-) -> tuple[str, str]:
+) -> tuple[str, str, str]:
     public_content: list[str] = []
     protected_content: list[str] = []
     decode_varint: list[str] = []
@@ -882,11 +996,11 @@ def build_message_type(
             f"static constexpr uint16_t ESTIMATED_SIZE = {estimated_size};"
         )
 
-        # Add message_name method for debugging
+        # Add message_name method inline in header
         public_content.append("#ifdef HAS_PROTO_MESSAGE_DUMP")
         snake_name = camel_to_snake(desc.name)
         public_content.append(
-            f'static constexpr const char *message_name() {{ return "{snake_name}"; }}'
+            f'const char *message_name() const override {{ return "{snake_name}"; }}'
         )
         public_content.append("#endif")
 
@@ -959,63 +1073,62 @@ def build_message_type(
         prot = "bool decode_64bit(uint32_t field_id, Proto64Bit value) override;"
         protected_content.insert(0, prot)
 
-    o = f"void {desc.name}::encode(ProtoWriteBuffer buffer) const {{"
+    # Only generate encode method if there are fields to encode
     if encode:
+        o = f"void {desc.name}::encode(ProtoWriteBuffer buffer) const {{"
         if len(encode) == 1 and len(encode[0]) + len(o) + 3 < 120:
             o += f" {encode[0]} "
         else:
             o += "\n"
             o += indent("\n".join(encode)) + "\n"
-    o += "}\n"
-    cpp += o
-    prot = "void encode(ProtoWriteBuffer buffer) const override;"
-    public_content.append(prot)
+        o += "}\n"
+        cpp += o
+        prot = "void encode(ProtoWriteBuffer buffer) const override;"
+        public_content.append(prot)
+    # If no fields to encode, the default implementation in ProtoMessage will be used
 
-    # Add calculate_size method
-    o = f"void {desc.name}::calculate_size(uint32_t &total_size) const {{"
-
-    # Add a check for empty/default objects to short-circuit the calculation
-    # Only add this optimization if we have fields to check
+    # Add calculate_size method only if there are fields
     if size_calc:
+        o = f"void {desc.name}::calculate_size(uint32_t &total_size) const {{"
         # For a single field, just inline it for simplicity
         if len(size_calc) == 1 and len(size_calc[0]) + len(o) + 3 < 120:
             o += f" {size_calc[0]} "
         else:
-            # For multiple fields, add a short-circuit check
+            # For multiple fields
             o += "\n"
-            # Performance optimization: add all the size calculations
             o += indent("\n".join(size_calc)) + "\n"
-    o += "}\n"
-    cpp += o
-    prot = "void calculate_size(uint32_t &total_size) const override;"
-    public_content.append(prot)
+        o += "}\n"
+        cpp += o
+        prot = "void calculate_size(uint32_t &total_size) const override;"
+        public_content.append(prot)
+    # If no fields to calculate size for, the default implementation in ProtoMessage will be used
 
-    o = f"void {desc.name}::dump_to(std::string &out) const {{"
-    if dump:
-        if len(dump) == 1 and len(dump[0]) + len(o) + 3 < 120:
-            o += f" {dump[0]} "
-        else:
-            o += "\n"
-            o += "  __attribute__((unused)) char buffer[64];\n"
-            o += f'  out.append("{desc.name} {{\\n");\n'
-            o += indent("\n".join(dump)) + "\n"
-            o += '  out.append("}");\n'
-    else:
-        o2 = f'out.append("{desc.name} {{}}");'
-        if len(o) + len(o2) + 3 < 120:
-            o += f" {o2} "
-        else:
-            o += "\n"
-            o += f"  {o2}\n"
-    o += "}\n"
-    cpp += "#ifdef HAS_PROTO_MESSAGE_DUMP\n"
-    cpp += o
-    cpp += "#endif\n"
+    # dump_to method declaration in header
     prot = "#ifdef HAS_PROTO_MESSAGE_DUMP\n"
     prot += "void dump_to(std::string &out) const override;\n"
     prot += "#endif\n"
     public_content.append(prot)
 
+    # dump_to implementation will go in dump_cpp
+    dump_impl = f"void {desc.name}::dump_to(std::string &out) const {{"
+    if dump:
+        if len(dump) == 1 and len(dump[0]) + len(dump_impl) + 3 < 120:
+            dump_impl += f" {dump[0]} "
+        else:
+            dump_impl += "\n"
+            dump_impl += "  __attribute__((unused)) char buffer[64];\n"
+            dump_impl += f'  out.append("{desc.name} {{\\n");\n'
+            dump_impl += indent("\n".join(dump)) + "\n"
+            dump_impl += '  out.append("}");\n'
+    else:
+        o2 = f'out.append("{desc.name} {{}}");'
+        if len(dump_impl) + len(o2) + 3 < 120:
+            dump_impl += f" {o2} "
+        else:
+            dump_impl += "\n"
+            dump_impl += f"  {o2}\n"
+    dump_impl += "}\n"
+
     if base_class:
         out = f"class {desc.name} : public {base_class} {{\n"
     else:
@@ -1028,14 +1141,18 @@ def build_message_type(
     if len(protected_content) > 0:
         out += "\n"
     out += "};\n"
-    return out, cpp
+
+    # Build dump_cpp content with dump_to implementation
+    dump_cpp = dump_impl
+
+    return out, cpp, dump_cpp
 
 
 SOURCE_BOTH = 0
 SOURCE_SERVER = 1
 SOURCE_CLIENT = 2
 
-RECEIVE_CASES: dict[int, str] = {}
+RECEIVE_CASES: dict[int, tuple[str, str | None]] = {}
 
 ifdefs: dict[str, str] = {}
 
@@ -1116,7 +1233,7 @@ def find_common_fields(
 def build_base_class(
     base_class_name: str,
     common_fields: list[descriptor.FieldDescriptorProto],
-) -> tuple[str, str]:
+) -> tuple[str, str, str]:
     """Build the base class definition and implementation."""
     public_content = []
     protected_content = []
@@ -1153,16 +1270,18 @@ def build_base_class(
     out += "};\n"
 
     # No implementation needed for base classes
+    dump_cpp = ""
 
-    return out, cpp
+    return out, cpp, dump_cpp
 
 
 def generate_base_classes(
     base_class_groups: dict[str, list[descriptor.DescriptorProto]],
-) -> tuple[str, str]:
+) -> tuple[str, str, str]:
     """Generate all base classes."""
     all_headers = []
     all_cpp = []
+    all_dump_cpp = []
 
     for base_class_name, messages in base_class_groups.items():
         # Find common fields
@@ -1170,11 +1289,12 @@ def generate_base_classes(
 
         if common_fields:
             # Generate base class
-            header, cpp = build_base_class(base_class_name, common_fields)
+            header, cpp, dump_cpp = build_base_class(base_class_name, common_fields)
             all_headers.append(header)
             all_cpp.append(cpp)
+            all_dump_cpp.append(dump_cpp)
 
-    return "\n".join(all_headers), "\n".join(all_cpp)
+    return "\n".join(all_headers), "\n".join(all_cpp), "\n".join(all_dump_cpp)
 
 
 def build_service_message_type(
@@ -1209,8 +1329,6 @@ def build_service_message_type(
         func = f"on_{snake}"
         hout += f"virtual void {func}(const {mt.name} &value){{}};\n"
         case = ""
-        if ifdef is not None:
-            case += f"#ifdef {ifdef}\n"
         case += f"{mt.name} msg;\n"
         case += "msg.decode(msg_data, msg_size);\n"
         if log:
@@ -1218,10 +1336,9 @@ def build_service_message_type(
             case += f'ESP_LOGVV(TAG, "{func}: %s", msg.dump().c_str());\n'
             case += "#endif\n"
         case += f"this->{func}(msg);\n"
-        if ifdef is not None:
-            case += "#endif\n"
         case += "break;"
-        RECEIVE_CASES[id_] = case
+        # Store the ifdef with the case for later use
+        RECEIVE_CASES[id_] = (case, ifdef)
 
         # Only close ifdef if we opened it
         if ifdef is not None:
@@ -1244,35 +1361,75 @@ def main() -> None:
     file = d.file[0]
     content = FILE_HEADER
     content += """\
-    #pragma once
+#pragma once
 
-    #include "proto.h"
-    #include "api_pb2_size.h"
+#include "esphome/core/defines.h"
 
-    namespace esphome {
-    namespace api {
+#include "proto.h"
+#include "api_pb2_size.h"
 
-    """
+namespace esphome {
+namespace api {
+
+"""
 
     cpp = FILE_HEADER
     cpp += """\
     #include "api_pb2.h"
     #include "api_pb2_size.h"
     #include "esphome/core/log.h"
+    #include "esphome/core/helpers.h"
 
-    #include 
+namespace esphome {
+namespace api {
 
-    namespace esphome {
-    namespace api {
+"""
 
-    """
+    # Initialize dump cpp content
+    dump_cpp = FILE_HEADER
+    dump_cpp += """\
+#include "api_pb2.h"
+#include "esphome/core/helpers.h"
+
+#include 
+
+#ifdef HAS_PROTO_MESSAGE_DUMP
+
+namespace esphome {
+namespace api {
+
+"""
 
     content += "namespace enums {\n\n"
 
+    # Build dynamic ifdef mappings for both enums and messages
+    enum_ifdef_map, message_ifdef_map = build_type_usage_map(file)
+
+    # Simple grouping of enums by ifdef
+    current_ifdef = None
+
     for enum in file.enum_type:
-        s, c = build_enum_type(enum)
+        s, c, dc = build_enum_type(enum, enum_ifdef_map)
+        enum_ifdef = enum_ifdef_map.get(enum.name)
+
+        # Handle ifdef changes
+        if enum_ifdef != current_ifdef:
+            if current_ifdef is not None:
+                content += "#endif\n"
+                dump_cpp += "#endif\n"
+            if enum_ifdef is not None:
+                content += f"#ifdef {enum_ifdef}\n"
+                dump_cpp += f"#ifdef {enum_ifdef}\n"
+            current_ifdef = enum_ifdef
+
         content += s
         cpp += c
+        dump_cpp += dc
+
+    # Close last ifdef
+    if current_ifdef is not None:
+        content += "#endif\n"
+        dump_cpp += "#endif\n"
 
     content += "\n}  // namespace enums\n\n"
 
@@ -1290,26 +1447,61 @@ def main() -> None:
 
     # Generate base classes
     if base_class_fields:
-        base_headers, base_cpp = generate_base_classes(base_class_groups)
+        base_headers, base_cpp, base_dump_cpp = generate_base_classes(base_class_groups)
         content += base_headers
         cpp += base_cpp
+        dump_cpp += base_dump_cpp
 
     # Generate message types with base class information
+    # Simple grouping by ifdef
+    current_ifdef = None
+
     for m in mt:
-        s, c = build_message_type(m, base_class_fields)
+        s, c, dc = build_message_type(m, base_class_fields)
+        msg_ifdef = message_ifdef_map.get(m.name)
+
+        # Handle ifdef changes
+        if msg_ifdef != current_ifdef:
+            if current_ifdef is not None:
+                content += "#endif\n"
+                if cpp:
+                    cpp += "#endif\n"
+                if dump_cpp:
+                    dump_cpp += "#endif\n"
+            if msg_ifdef is not None:
+                content += f"#ifdef {msg_ifdef}\n"
+                cpp += f"#ifdef {msg_ifdef}\n"
+                dump_cpp += f"#ifdef {msg_ifdef}\n"
+            current_ifdef = msg_ifdef
+
         content += s
         cpp += c
+        dump_cpp += dc
+
+    # Close last ifdef
+    if current_ifdef is not None:
+        content += "#endif\n"
+        cpp += "#endif\n"
+        dump_cpp += "#endif\n"
 
     content += """\
 
-    }  // namespace api
-    }  // namespace esphome
-    """
+}  // namespace api
+}  // namespace esphome
+"""
     cpp += """\
 
-    }  // namespace api
-    }  // namespace esphome
-    """
+}  // namespace api
+}  // namespace esphome
+"""
+
+    dump_cpp += """\
+
+}  // namespace api
+}  // namespace esphome
+
+#endif  // HAS_PROTO_MESSAGE_DUMP
+"""
 
     with open(root / "api_pb2.h", "w", encoding="utf-8") as f:
         f.write(content)
@@ -1317,29 +1509,33 @@ def main() -> None:
     with open(root / "api_pb2.cpp", "w", encoding="utf-8") as f:
         f.write(cpp)
 
+    with open(root / "api_pb2_dump.cpp", "w", encoding="utf-8") as f:
+        f.write(dump_cpp)
+
     hpp = FILE_HEADER
     hpp += """\
-    #pragma once
+#pragma once
 
-    #include "api_pb2.h"
-    #include "esphome/core/defines.h"
+#include "esphome/core/defines.h"
 
-    namespace esphome {
-    namespace api {
+#include "api_pb2.h"
 
-    """
+namespace esphome {
+namespace api {
+
+"""
 
     cpp = FILE_HEADER
     cpp += """\
-    #include "api_pb2_service.h"
-    #include "esphome/core/log.h"
+#include "api_pb2_service.h"
+#include "esphome/core/log.h"
 
-    namespace esphome {
-    namespace api {
+namespace esphome {
+namespace api {
 
-    static const char *const TAG = "api.service";
+static const char *const TAG = "api.service";
 
-    """
+"""
 
     class_name = "APIServerConnectionBase"
 
@@ -1357,7 +1553,7 @@ def main() -> None:
     hpp += "  template\n"
     hpp += "  bool send_message(const T &msg) {\n"
     hpp += "#ifdef HAS_PROTO_MESSAGE_DUMP\n"
-    hpp += "    this->log_send_message_(T::message_name(), msg.dump());\n"
+    hpp += "    this->log_send_message_(msg.message_name(), msg.dump());\n"
     hpp += "#endif\n"
     hpp += "    return this->send_message_(msg, T::MESSAGE_TYPE);\n"
     hpp += "  }\n\n"
@@ -1380,18 +1576,21 @@ def main() -> None:
     cases = list(RECEIVE_CASES.items())
     cases.sort()
     hpp += " protected:\n"
-    hpp += "  bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;\n"
-    out = f"bool {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {{\n"
+    hpp += "  void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;\n"
+    out = f"void {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {{\n"
     out += "  switch (msg_type) {\n"
-    for i, case in cases:
-        c = f"case {i}: {{\n"
-        c += indent(case) + "\n"
-        c += "}"
-        out += indent(c, "    ") + "\n"
+    for i, (case, ifdef) in cases:
+        if ifdef is not None:
+            out += f"#ifdef {ifdef}\n"
+        c = f"    case {i}: {{\n"
+        c += indent(case, "      ") + "\n"
+        c += "    }"
+        out += c + "\n"
+        if ifdef is not None:
+            out += "#endif\n"
     out += "    default:\n"
-    out += "      return false;\n"
+    out += "      break;\n"
     out += "  }\n"
-    out += "  return true;\n"
     out += "}\n"
     cpp += out
     hpp += "};\n"
@@ -1415,7 +1614,7 @@ def main() -> None:
         needs_conn = get_opt(m, pb.needs_setup_connection, True)
         needs_auth = get_opt(m, pb.needs_authentication, True)
 
-        ifdef = ifdefs.get(inp, None)
+        ifdef = message_ifdef_map.get(inp, ifdefs.get(inp, None))
 
         if ifdef is not None:
             hpp += f"#ifdef {ifdef}\n"
@@ -1425,25 +1624,40 @@ def main() -> None:
         hpp_protected += f"  void {on_func}(const {inp} &msg) override;\n"
         hpp += f"  virtual {ret} {func}(const {inp} &msg) = 0;\n"
         cpp += f"void {class_name}::{on_func}(const {inp} &msg) {{\n"
-        body = ""
-        if needs_conn:
-            body += "if (!this->is_connection_setup()) {\n"
-            body += "  this->on_no_setup_connection();\n"
-            body += "  return;\n"
-            body += "}\n"
-        if needs_auth:
-            body += "if (!this->is_authenticated()) {\n"
-            body += "  this->on_unauthenticated_access();\n"
-            body += "  return;\n"
-            body += "}\n"
 
-        if is_void:
-            body += f"this->{func}(msg);\n"
-        else:
-            body += f"{ret} ret = this->{func}(msg);\n"
-            body += "if (!this->send_message(ret)) {\n"
-            body += "  this->on_fatal_error();\n"
+        # Start with authentication/connection check if needed
+        if needs_auth or needs_conn:
+            # Determine which check to use
+            if needs_auth:
+                check_func = "this->check_authenticated_()"
+            else:
+                check_func = "this->check_connection_setup_()"
+
+            body = f"if ({check_func}) {{\n"
+
+            # Add the actual handler code, indented
+            handler_body = ""
+            if is_void:
+                handler_body = f"this->{func}(msg);\n"
+            else:
+                handler_body = f"{ret} ret = this->{func}(msg);\n"
+                handler_body += "if (!this->send_message(ret)) {\n"
+                handler_body += "  this->on_fatal_error();\n"
+                handler_body += "}\n"
+
+            body += indent(handler_body) + "\n"
             body += "}\n"
+        else:
+            # No auth check needed, just call the handler
+            body = ""
+            if is_void:
+                body += f"this->{func}(msg);\n"
+            else:
+                body += f"{ret} ret = this->{func}(msg);\n"
+                body += "if (!this->send_message(ret)) {\n"
+                body += "  this->on_fatal_error();\n"
+                body += "}\n"
+
         cpp += indent(body) + "\n" + "}\n"
 
         if ifdef is not None:
@@ -1457,14 +1671,14 @@ def main() -> None:
 
     hpp += """\
 
-    }  // namespace api
-    }  // namespace esphome
-    """
+}  // namespace api
+}  // namespace esphome
+"""
     cpp += """\
 
-    }  // namespace api
-    }  // namespace esphome
-    """
+}  // namespace api
+}  // namespace esphome
+"""
 
     with open(root / "api_pb2_service.h", "w", encoding="utf-8") as f:
         f.write(hpp)
@@ -1487,6 +1701,8 @@ def main() -> None:
         exec_clang_format(root / "api_pb2_service.cpp")
         exec_clang_format(root / "api_pb2.h")
         exec_clang_format(root / "api_pb2.cpp")
+        exec_clang_format(root / "api_pb2_dump.h")
+        exec_clang_format(root / "api_pb2_dump.cpp")
     except ImportError:
         pass
 
diff --git a/script/ci-custom.py b/script/ci-custom.py
index fbabbc1e74..d0b518251f 100755
--- a/script/ci-custom.py
+++ b/script/ci-custom.py
@@ -559,6 +559,12 @@ def lint_relative_py_import(fname):
         "esphome/components/libretiny/core.cpp",
         "esphome/components/host/core.cpp",
         "esphome/components/zephyr/core.cpp",
+        "esphome/components/esp32/helpers.cpp",
+        "esphome/components/esp8266/helpers.cpp",
+        "esphome/components/rp2040/helpers.cpp",
+        "esphome/components/libretiny/helpers.cpp",
+        "esphome/components/host/helpers.cpp",
+        "esphome/components/zephyr/helpers.cpp",
         "esphome/components/http_request/httplib.h",
     ],
 )
diff --git a/script/run-in-env.py b/script/run-in-env.py
old mode 100644
new mode 100755
diff --git a/tests/component_tests/conftest.py b/tests/component_tests/conftest.py
index 7aa7dfe698..b1e0eaa200 100644
--- a/tests/component_tests/conftest.py
+++ b/tests/component_tests/conftest.py
@@ -1,29 +1,71 @@
 """Fixtures for component tests."""
 
+from __future__ import annotations
+
+from collections.abc import Callable, Generator
 from pathlib import Path
 import sys
 
+import pytest
+
 # Add package root to python path
 here = Path(__file__).parent
 package_root = here.parent.parent
 sys.path.insert(0, package_root.as_posix())
 
-import pytest  # noqa: E402
-
 from esphome.__main__ import generate_cpp_contents  # noqa: E402
 from esphome.config import read_config  # noqa: E402
 from esphome.core import CORE  # noqa: E402
 
 
+@pytest.fixture(autouse=True)
+def config_path(request: pytest.FixtureRequest) -> Generator[None]:
+    """Set CORE.config_path to the component's config directory and reset it after the test."""
+    original_path = CORE.config_path
+    config_dir = Path(request.fspath).parent / "config"
+
+    # Check if config directory exists, if not use parent directory
+    if config_dir.exists():
+        # Set config_path to a dummy yaml file in the config directory
+        # This ensures CORE.config_dir points to the config directory
+        CORE.config_path = str(config_dir / "dummy.yaml")
+    else:
+        CORE.config_path = str(Path(request.fspath).parent / "dummy.yaml")
+
+    yield
+    CORE.config_path = original_path
+
+
 @pytest.fixture
-def generate_main():
+def component_fixture_path(request: pytest.FixtureRequest) -> Callable[[str], Path]:
+    """Return a function to get absolute paths relative to the component's fixtures directory."""
+
+    def _get_path(file_name: str) -> Path:
+        """Get the absolute path of a file relative to the component's fixtures directory."""
+        return (Path(request.fspath).parent / "fixtures" / file_name).absolute()
+
+    return _get_path
+
+
+@pytest.fixture
+def component_config_path(request: pytest.FixtureRequest) -> Callable[[str], Path]:
+    """Return a function to get absolute paths relative to the component's config directory."""
+
+    def _get_path(file_name: str) -> Path:
+        """Get the absolute path of a file relative to the component's config directory."""
+        return (Path(request.fspath).parent / "config" / file_name).absolute()
+
+    return _get_path
+
+
+@pytest.fixture
+def generate_main() -> Generator[Callable[[str | Path], str]]:
     """Generates the C++ main.cpp file and returns it in string form."""
 
-    def generator(path: str) -> str:
-        CORE.config_path = path
+    def generator(path: str | Path) -> str:
+        CORE.config_path = str(path)
         CORE.config = read_config({})
         generate_cpp_contents(CORE.config)
-        print(CORE.cpp_main_section)
         return CORE.cpp_main_section
 
     yield generator
diff --git a/tests/component_tests/image/config/bad.png b/tests/component_tests/image/config/bad.png
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tests/component_tests/image/config/image.png b/tests/component_tests/image/config/image.png
new file mode 100644
index 0000000000..bd2fd54783
Binary files /dev/null and b/tests/component_tests/image/config/image.png differ
diff --git a/tests/component_tests/image/config/image_test.yaml b/tests/component_tests/image/config/image_test.yaml
new file mode 100644
index 0000000000..3ff1260bd0
--- /dev/null
+++ b/tests/component_tests/image/config/image_test.yaml
@@ -0,0 +1,20 @@
+esphome:
+  name: test
+
+esp32:
+  board: esp32s3box
+
+image:
+  - file: image.png
+    byte_order: little_endian
+    id: cat_img
+    type: rgb565
+
+spi:
+  mosi_pin: 6
+  clk_pin: 7
+
+display:
+  - platform: mipi_spi
+    id: lcd_display
+    model: s3box
diff --git a/tests/component_tests/image/test_init.py b/tests/component_tests/image/test_init.py
new file mode 100644
index 0000000000..d8a883d32f
--- /dev/null
+++ b/tests/component_tests/image/test_init.py
@@ -0,0 +1,183 @@
+"""Tests for image configuration validation."""
+
+from __future__ import annotations
+
+from collections.abc import Callable
+from pathlib import Path
+from typing import Any
+
+import pytest
+
+from esphome import config_validation as cv
+from esphome.components.image import CONFIG_SCHEMA
+
+
+@pytest.mark.parametrize(
+    ("config", "error_match"),
+    [
+        pytest.param(
+            "a string",
+            "Badly formed image configuration, expected a list or a dictionary",
+            id="invalid_string_config",
+        ),
+        pytest.param(
+            {"id": "image_id", "type": "rgb565"},
+            r"required key not provided @ data\[0\]\['file'\]",
+            id="missing_file",
+        ),
+        pytest.param(
+            {"file": "image.png", "type": "rgb565"},
+            r"required key not provided @ data\[0\]\['id'\]",
+            id="missing_id",
+        ),
+        pytest.param(
+            {"id": "mdi_id", "file": "mdi:weather-##", "type": "rgb565"},
+            "Could not parse mdi icon name",
+            id="invalid_mdi_icon",
+        ),
+        pytest.param(
+            {
+                "id": "image_id",
+                "file": "image.png",
+                "type": "binary",
+                "transparency": "alpha_channel",
+            },
+            "Image format 'BINARY' cannot have transparency",
+            id="binary_with_transparency",
+        ),
+        pytest.param(
+            {
+                "id": "image_id",
+                "file": "image.png",
+                "type": "rgb565",
+                "transparency": "chroma_key",
+                "invert_alpha": True,
+            },
+            "No alpha channel to invert",
+            id="invert_alpha_without_alpha_channel",
+        ),
+        pytest.param(
+            {
+                "id": "image_id",
+                "file": "image.png",
+                "type": "binary",
+                "byte_order": "big_endian",
+            },
+            "Image format 'BINARY' does not support byte order configuration",
+            id="binary_with_byte_order",
+        ),
+        pytest.param(
+            {"id": "image_id", "file": "bad.png", "type": "binary"},
+            "File can't be opened as image",
+            id="invalid_image_file",
+        ),
+        pytest.param(
+            {"defaults": {}, "images": [{"id": "image_id", "file": "image.png"}]},
+            "Type is required either in the image config or in the defaults",
+            id="missing_type_in_defaults",
+        ),
+    ],
+)
+def test_image_configuration_errors(
+    config: Any,
+    error_match: str,
+) -> None:
+    """Test detection of invalid configuration."""
+    with pytest.raises(cv.Invalid, match=error_match):
+        CONFIG_SCHEMA(config)
+
+
+@pytest.mark.parametrize(
+    "config",
+    [
+        pytest.param(
+            {
+                "id": "image_id",
+                "file": "image.png",
+                "type": "rgb565",
+                "transparency": "chroma_key",
+                "byte_order": "little_endian",
+                "dither": "FloydSteinberg",
+                "resize": "100x100",
+                "invert_alpha": False,
+            },
+            id="single_image_all_options",
+        ),
+        pytest.param(
+            [
+                {
+                    "id": "image_id",
+                    "file": "image.png",
+                    "type": "binary",
+                }
+            ],
+            id="list_of_images",
+        ),
+        pytest.param(
+            {
+                "defaults": {
+                    "type": "rgb565",
+                    "transparency": "chroma_key",
+                    "byte_order": "little_endian",
+                    "dither": "FloydSteinberg",
+                    "resize": "100x100",
+                    "invert_alpha": False,
+                },
+                "images": [
+                    {
+                        "id": "image_id",
+                        "file": "image.png",
+                    }
+                ],
+            },
+            id="images_with_defaults",
+        ),
+        pytest.param(
+            {
+                "rgb565": {
+                    "alpha_channel": [
+                        {
+                            "id": "image_id",
+                            "file": "image.png",
+                            "transparency": "alpha_channel",
+                            "byte_order": "little_endian",
+                            "dither": "FloydSteinberg",
+                            "resize": "100x100",
+                            "invert_alpha": False,
+                        }
+                    ]
+                },
+                "binary": [
+                    {
+                        "id": "image_id",
+                        "file": "image.png",
+                        "transparency": "opaque",
+                        "dither": "FloydSteinberg",
+                        "resize": "100x100",
+                        "invert_alpha": False,
+                    }
+                ],
+            },
+            id="type_based_organization",
+        ),
+    ],
+)
+def test_image_configuration_success(
+    config: dict[str, Any] | list[dict[str, Any]],
+) -> None:
+    """Test successful configuration validation."""
+    CONFIG_SCHEMA(config)
+
+
+def test_image_generation(
+    generate_main: Callable[[str | Path], str],
+    component_config_path: Callable[[str], Path],
+) -> None:
+    """Test image generation configuration."""
+
+    main_cpp = generate_main(component_config_path("image_test.yaml"))
+    assert "uint8_t_id[] PROGMEM = {0x24, 0x21, 0x24, 0x21" in main_cpp
+    assert (
+        "cat_img = new image::Image(uint8_t_id, 32, 24, image::IMAGE_TYPE_RGB565, image::TRANSPARENCY_OPAQUE);"
+        in main_cpp
+    )
diff --git a/tests/component_tests/ota/test_web_server_ota.py b/tests/component_tests/ota/test_web_server_ota.py
new file mode 100644
index 0000000000..0d8ff6f134
--- /dev/null
+++ b/tests/component_tests/ota/test_web_server_ota.py
@@ -0,0 +1,102 @@
+"""Tests for the web_server OTA platform."""
+
+from collections.abc import Callable
+
+
+def test_web_server_ota_generated(generate_main: Callable[[str], str]) -> None:
+    """Test that web_server OTA platform generates correct code."""
+    main_cpp = generate_main("tests/component_tests/ota/test_web_server_ota.yaml")
+
+    # Check that the web server OTA component is included
+    assert "WebServerOTAComponent" in main_cpp
+    assert "web_server::WebServerOTAComponent" in main_cpp
+
+    # Check that global web server base is referenced
+    assert "global_web_server_base" in main_cpp
+
+    # Check component is registered
+    assert "App.register_component(web_server_webserverotacomponent_id)" in main_cpp
+
+
+def test_web_server_ota_with_callbacks(generate_main: Callable[[str], str]) -> None:
+    """Test web_server OTA with state callbacks."""
+    main_cpp = generate_main(
+        "tests/component_tests/ota/test_web_server_ota_callbacks.yaml"
+    )
+
+    # Check that web server OTA component is present
+    assert "WebServerOTAComponent" in main_cpp
+
+    # Check that callbacks are configured
+    # The actual callback code is in the component implementation, not main.cpp
+    # But we can check that logger.log statements are present from the callbacks
+    assert "logger.log" in main_cpp
+    assert "OTA started" in main_cpp
+    assert "OTA completed" in main_cpp
+    assert "OTA error" in main_cpp
+
+
+def test_web_server_ota_idf_multipart(generate_main: Callable[[str], str]) -> None:
+    """Test that ESP-IDF builds include multipart parser dependency."""
+    main_cpp = generate_main("tests/component_tests/ota/test_web_server_ota_idf.yaml")
+
+    # Check that web server OTA component is present
+    assert "WebServerOTAComponent" in main_cpp
+
+    # For ESP-IDF builds, the framework type is esp-idf
+    # The multipart parser dependency is added by web_server_idf
+    assert "web_server::WebServerOTAComponent" in main_cpp
+
+
+def test_web_server_ota_without_web_server_fails(
+    generate_main: Callable[[str], str],
+) -> None:
+    """Test that web_server OTA requires web_server component."""
+    # This should fail during validation since web_server_base is required
+    # but we can't test validation failures with generate_main
+    # Instead, verify that both components are needed in valid config
+    main_cpp = generate_main("tests/component_tests/ota/test_web_server_ota.yaml")
+
+    # Both web server and OTA components should be present
+    assert "WebServer" in main_cpp
+    assert "WebServerOTAComponent" in main_cpp
+
+
+def test_multiple_ota_platforms(generate_main: Callable[[str], str]) -> None:
+    """Test multiple OTA platforms can coexist."""
+    main_cpp = generate_main("tests/component_tests/ota/test_web_server_ota_multi.yaml")
+
+    # Check all OTA platforms are included
+    assert "WebServerOTAComponent" in main_cpp
+    assert "ESPHomeOTAComponent" in main_cpp
+    assert "OtaHttpRequestComponent" in main_cpp
+
+    # Check components are from correct namespaces
+    assert "web_server::WebServerOTAComponent" in main_cpp
+    assert "esphome::ESPHomeOTAComponent" in main_cpp
+    assert "http_request::OtaHttpRequestComponent" in main_cpp
+
+
+def test_web_server_ota_arduino_with_auth(generate_main: Callable[[str], str]) -> None:
+    """Test web_server OTA with Arduino framework and authentication."""
+    main_cpp = generate_main(
+        "tests/component_tests/ota/test_web_server_ota_arduino.yaml"
+    )
+
+    # Check web server OTA component is present
+    assert "WebServerOTAComponent" in main_cpp
+
+    # Check authentication is set up for web server
+    assert "set_auth_username" in main_cpp
+    assert "set_auth_password" in main_cpp
+
+
+def test_web_server_ota_esp8266(generate_main: Callable[[str], str]) -> None:
+    """Test web_server OTA on ESP8266 platform."""
+    main_cpp = generate_main(
+        "tests/component_tests/ota/test_web_server_ota_esp8266.yaml"
+    )
+
+    # Check web server OTA component is present
+    assert "WebServerOTAComponent" in main_cpp
+    assert "web_server::WebServerOTAComponent" in main_cpp
diff --git a/tests/component_tests/ota/test_web_server_ota.yaml b/tests/component_tests/ota/test_web_server_ota.yaml
new file mode 100644
index 0000000000..e0fda3d0b5
--- /dev/null
+++ b/tests/component_tests/ota/test_web_server_ota.yaml
@@ -0,0 +1,15 @@
+esphome:
+  name: test_web_server_ota
+
+esp32:
+  board: esp32dev
+
+wifi:
+  ssid: MySSID
+  password: password1
+
+web_server:
+  port: 80
+
+ota:
+  - platform: web_server
diff --git a/tests/component_tests/ota/test_web_server_ota_arduino.yaml b/tests/component_tests/ota/test_web_server_ota_arduino.yaml
new file mode 100644
index 0000000000..9462548cc8
--- /dev/null
+++ b/tests/component_tests/ota/test_web_server_ota_arduino.yaml
@@ -0,0 +1,18 @@
+esphome:
+  name: test_web_server_ota_arduino
+
+esp32:
+  board: esp32dev
+
+wifi:
+  ssid: MySSID
+  password: password1
+
+web_server:
+  port: 80
+  auth:
+    username: admin
+    password: admin
+
+ota:
+  - platform: web_server
diff --git a/tests/component_tests/ota/test_web_server_ota_callbacks.yaml b/tests/component_tests/ota/test_web_server_ota_callbacks.yaml
new file mode 100644
index 0000000000..c2fd9e0f19
--- /dev/null
+++ b/tests/component_tests/ota/test_web_server_ota_callbacks.yaml
@@ -0,0 +1,31 @@
+esphome:
+  name: test_web_server_ota_callbacks
+
+esp32:
+  board: esp32dev
+
+wifi:
+  ssid: MySSID
+  password: password1
+
+logger:
+
+web_server:
+  port: 80
+
+ota:
+  - platform: web_server
+    on_begin:
+      - logger.log: "OTA started"
+    on_progress:
+      - logger.log:
+          format: "OTA progress: %.1f%%"
+          args: ["x"]
+    on_end:
+      - logger.log: "OTA completed"
+    on_error:
+      - logger.log:
+          format: "OTA error: %d"
+          args: ["x"]
+    on_state_change:
+      - logger.log: "OTA state changed"
diff --git a/tests/component_tests/ota/test_web_server_ota_esp8266.yaml b/tests/component_tests/ota/test_web_server_ota_esp8266.yaml
new file mode 100644
index 0000000000..a1b66a5b53
--- /dev/null
+++ b/tests/component_tests/ota/test_web_server_ota_esp8266.yaml
@@ -0,0 +1,15 @@
+esphome:
+  name: test_web_server_ota_esp8266
+
+esp8266:
+  board: nodemcuv2
+
+wifi:
+  ssid: MySSID
+  password: password1
+
+web_server:
+  port: 80
+
+ota:
+  - platform: web_server
diff --git a/tests/component_tests/ota/test_web_server_ota_idf.yaml b/tests/component_tests/ota/test_web_server_ota_idf.yaml
new file mode 100644
index 0000000000..18b639347c
--- /dev/null
+++ b/tests/component_tests/ota/test_web_server_ota_idf.yaml
@@ -0,0 +1,17 @@
+esphome:
+  name: test_web_server_ota_idf
+
+esp32:
+  board: esp32dev
+  framework:
+    type: esp-idf
+
+wifi:
+  ssid: MySSID
+  password: password1
+
+web_server:
+  port: 80
+
+ota:
+  - platform: web_server
diff --git a/tests/component_tests/ota/test_web_server_ota_multi.yaml b/tests/component_tests/ota/test_web_server_ota_multi.yaml
new file mode 100644
index 0000000000..7926b09c71
--- /dev/null
+++ b/tests/component_tests/ota/test_web_server_ota_multi.yaml
@@ -0,0 +1,21 @@
+esphome:
+  name: test_web_server_ota_multi
+
+esp32:
+  board: esp32dev
+
+wifi:
+  ssid: MySSID
+  password: password1
+
+web_server:
+  port: 80
+
+http_request:
+  verify_ssl: false
+
+ota:
+  - platform: esphome
+    password: "test_password"
+  - platform: web_server
+  - platform: http_request
diff --git a/tests/component_tests/text/test_text.py b/tests/component_tests/text/test_text.py
index 51fcb3d382..75f1c4b88b 100644
--- a/tests/component_tests/text/test_text.py
+++ b/tests/component_tests/text/test_text.py
@@ -66,5 +66,5 @@ def test_text_config_lamda_is_set(generate_main):
     main_cpp = generate_main("tests/component_tests/text/test_text.yaml")
 
     # Then
-    assert "it_4->set_template([=]() -> optional {" in main_cpp
+    assert "it_4->set_template([=]() -> esphome::optional {" in main_cpp
     assert 'return std::string{"Hello"};' in main_cpp
diff --git a/tests/component_tests/web_server/test_ota_migration.py b/tests/component_tests/web_server/test_ota_migration.py
new file mode 100644
index 0000000000..7f34ec75f6
--- /dev/null
+++ b/tests/component_tests/web_server/test_ota_migration.py
@@ -0,0 +1,38 @@
+"""Tests for web_server OTA migration validation."""
+
+import pytest
+
+from esphome import config_validation as cv
+from esphome.types import ConfigType
+
+
+def test_web_server_ota_true_fails_validation() -> None:
+    """Test that web_server with ota: true fails validation with helpful message."""
+    from esphome.components.web_server import validate_ota_removed
+
+    # Config with ota: true should fail
+    config: ConfigType = {"ota": True}
+
+    with pytest.raises(cv.Invalid) as exc_info:
+        validate_ota_removed(config)
+
+    # Check error message contains migration instructions
+    error_msg = str(exc_info.value)
+    assert "has been removed from 'web_server'" in error_msg
+    assert "platform: web_server" in error_msg
+    assert "ota:" in error_msg
+
+
+def test_web_server_ota_false_passes_validation() -> None:
+    """Test that web_server with ota: false passes validation."""
+    from esphome.components.web_server import validate_ota_removed
+
+    # Config with ota: false should pass
+    config: ConfigType = {"ota": False}
+    result = validate_ota_removed(config)
+    assert result == config
+
+    # Config without ota should also pass
+    config: ConfigType = {}
+    result = validate_ota_removed(config)
+    assert result == config
diff --git a/tests/components/adc/test.ln882x-ard.yaml b/tests/components/adc/test.ln882x-ard.yaml
new file mode 100644
index 0000000000..92c76ca9b3
--- /dev/null
+++ b/tests/components/adc/test.ln882x-ard.yaml
@@ -0,0 +1,4 @@
+sensor:
+  - platform: adc
+    pin: PA0
+    name: Basic ADC Test
diff --git a/tests/components/addressable_light/common-ard-esp32_rmt_led_strip.yaml b/tests/components/addressable_light/common-ard-esp32_rmt_led_strip.yaml
index 9c5e63cdc6..a071f9df91 100644
--- a/tests/components/addressable_light/common-ard-esp32_rmt_led_strip.yaml
+++ b/tests/components/addressable_light/common-ard-esp32_rmt_led_strip.yaml
@@ -6,7 +6,6 @@ light:
     rgb_order: GRB
     num_leds: 256
     pin: ${pin}
-    rmt_channel: 0
 
 display:
   - platform: addressable_light
diff --git a/tests/components/ade7880/common.yaml b/tests/components/ade7880/common.yaml
index 0aa388a325..48c22c8485 100644
--- a/tests/components/ade7880/common.yaml
+++ b/tests/components/ade7880/common.yaml
@@ -12,12 +12,12 @@ sensor:
     frequency: 60Hz
     phase_a:
       name: Channel A
-      voltage: Voltage
-      current: Current
-      active_power: Active Power
-      power_factor: Power Factor
-      forward_active_energy: Forward Active Energy
-      reverse_active_energy: Reverse Active Energy
+      voltage: Channel A Voltage
+      current: Channel A Current
+      active_power: Channel A Active Power
+      power_factor: Channel A Power Factor
+      forward_active_energy: Channel A Forward Active Energy
+      reverse_active_energy: Channel A Reverse Active Energy
       calibration:
         current_gain: 3116628
         voltage_gain: -757178
@@ -25,12 +25,12 @@ sensor:
         phase_angle: 188
     phase_b:
       name: Channel B
-      voltage: Voltage
-      current: Current
-      active_power: Active Power
-      power_factor: Power Factor
-      forward_active_energy: Forward Active Energy
-      reverse_active_energy: Reverse Active Energy
+      voltage: Channel B Voltage
+      current: Channel B Current
+      active_power: Channel B Active Power
+      power_factor: Channel B Power Factor
+      forward_active_energy: Channel B Forward Active Energy
+      reverse_active_energy: Channel B Reverse Active Energy
       calibration:
         current_gain: 3133655
         voltage_gain: -755235
@@ -38,12 +38,12 @@ sensor:
         phase_angle: 188
     phase_c:
       name: Channel C
-      voltage: Voltage
-      current: Current
-      active_power: Active Power
-      power_factor: Power Factor
-      forward_active_energy: Forward Active Energy
-      reverse_active_energy: Reverse Active Energy
+      voltage: Channel C Voltage
+      current: Channel C Current
+      active_power: Channel C Active Power
+      power_factor: Channel C Power Factor
+      forward_active_energy: Channel C Forward Active Energy
+      reverse_active_energy: Channel C Reverse Active Energy
       calibration:
         current_gain: 3111158
         voltage_gain: -743813
@@ -51,6 +51,6 @@ sensor:
         phase_angle: 180
     neutral:
       name: Neutral
-      current: Current
+      current: Neutral Current
       calibration:
         current_gain: 3189
diff --git a/tests/components/alarm_control_panel/common.yaml b/tests/components/alarm_control_panel/common.yaml
index 5b8ae5a282..142bf3c7e6 100644
--- a/tests/components/alarm_control_panel/common.yaml
+++ b/tests/components/alarm_control_panel/common.yaml
@@ -26,7 +26,7 @@ alarm_control_panel:
             ESP_LOGD("TEST", "State change %s", LOG_STR_ARG(alarm_control_panel_state_to_string(id(alarmcontrolpanel1)->get_state())));
   - platform: template
     id: alarmcontrolpanel2
-    name: Alarm Panel
+    name: Alarm Panel 2
     codes:
       - "1234"
     requires_code_to_arm: true
diff --git a/tests/components/binary_sensor/common.yaml b/tests/components/binary_sensor/common.yaml
new file mode 100644
index 0000000000..2b4a006352
--- /dev/null
+++ b/tests/components/binary_sensor/common.yaml
@@ -0,0 +1,40 @@
+binary_sensor:
+  - platform: template
+    trigger_on_initial_state: true
+    id: some_binary_sensor
+    name: "Random binary"
+    lambda: return (random_uint32() & 1) == 0;
+    filters:
+      - invert:
+      - delayed_on: 100ms
+      - delayed_off: 100ms
+      # Templated, delays for 1s (1000ms) only if a reed switch is active
+      - delayed_on_off: !lambda "return 1000;"
+      - delayed_on_off:
+          time_on: 10s
+          time_off: !lambda "return 1000;"
+      - autorepeat:
+          - delay: 1s
+            time_off: 100ms
+            time_on: 900ms
+          - delay: 5s
+            time_off: 100ms
+            time_on: 400ms
+      - lambda: |-
+          if (id(some_binary_sensor).state) {
+            return x;
+          } else {
+            return {};
+          }
+      - settle: 100ms
+      - timeout: 10s
+
+    on_state_change:
+      then:
+        - logger.log:
+            format: "Old state was %s"
+            args: ['x_previous.has_value() ? ONOFF(x_previous) : "Unknown"']
+        - logger.log:
+            format: "New state is %s"
+            args: ['x.has_value() ? ONOFF(x) : "Unknown"']
+        - binary_sensor.invalidate_state: some_binary_sensor
diff --git a/tests/components/binary_sensor/test.bk72xx-ard.yaml b/tests/components/binary_sensor/test.bk72xx-ard.yaml
new file mode 100644
index 0000000000..25cb37a0b4
--- /dev/null
+++ b/tests/components/binary_sensor/test.bk72xx-ard.yaml
@@ -0,0 +1,2 @@
+packages:
+  common: !include common.yaml
diff --git a/tests/components/binary_sensor/test.esp32-ard.yaml b/tests/components/binary_sensor/test.esp32-ard.yaml
new file mode 100644
index 0000000000..25cb37a0b4
--- /dev/null
+++ b/tests/components/binary_sensor/test.esp32-ard.yaml
@@ -0,0 +1,2 @@
+packages:
+  common: !include common.yaml
diff --git a/tests/components/binary_sensor/test.esp32-c3-ard.yaml b/tests/components/binary_sensor/test.esp32-c3-ard.yaml
new file mode 100644
index 0000000000..25cb37a0b4
--- /dev/null
+++ b/tests/components/binary_sensor/test.esp32-c3-ard.yaml
@@ -0,0 +1,2 @@
+packages:
+  common: !include common.yaml
diff --git a/tests/components/binary_sensor/test.esp32-c3-idf.yaml b/tests/components/binary_sensor/test.esp32-c3-idf.yaml
new file mode 100644
index 0000000000..25cb37a0b4
--- /dev/null
+++ b/tests/components/binary_sensor/test.esp32-c3-idf.yaml
@@ -0,0 +1,2 @@
+packages:
+  common: !include common.yaml
diff --git a/tests/components/binary_sensor/test.esp32-idf.yaml b/tests/components/binary_sensor/test.esp32-idf.yaml
new file mode 100644
index 0000000000..25cb37a0b4
--- /dev/null
+++ b/tests/components/binary_sensor/test.esp32-idf.yaml
@@ -0,0 +1,2 @@
+packages:
+  common: !include common.yaml
diff --git a/tests/components/binary_sensor/test.esp32-s3-idf.yaml b/tests/components/binary_sensor/test.esp32-s3-idf.yaml
new file mode 100644
index 0000000000..25cb37a0b4
--- /dev/null
+++ b/tests/components/binary_sensor/test.esp32-s3-idf.yaml
@@ -0,0 +1,2 @@
+packages:
+  common: !include common.yaml
diff --git a/tests/components/binary_sensor/test.esp8266-ard.yaml b/tests/components/binary_sensor/test.esp8266-ard.yaml
new file mode 100644
index 0000000000..25cb37a0b4
--- /dev/null
+++ b/tests/components/binary_sensor/test.esp8266-ard.yaml
@@ -0,0 +1,2 @@
+packages:
+  common: !include common.yaml
diff --git a/tests/components/binary_sensor/test.ln882x-ard.yaml b/tests/components/binary_sensor/test.ln882x-ard.yaml
new file mode 100644
index 0000000000..25cb37a0b4
--- /dev/null
+++ b/tests/components/binary_sensor/test.ln882x-ard.yaml
@@ -0,0 +1,2 @@
+packages:
+  common: !include common.yaml
diff --git a/tests/components/binary_sensor/test.rp2040-ard.yaml b/tests/components/binary_sensor/test.rp2040-ard.yaml
new file mode 100644
index 0000000000..25cb37a0b4
--- /dev/null
+++ b/tests/components/binary_sensor/test.rp2040-ard.yaml
@@ -0,0 +1,2 @@
+packages:
+  common: !include common.yaml
diff --git a/tests/components/binary_sensor_map/common.yaml b/tests/components/binary_sensor_map/common.yaml
index 8ffdd1f379..2fed5ae515 100644
--- a/tests/components/binary_sensor_map/common.yaml
+++ b/tests/components/binary_sensor_map/common.yaml
@@ -26,7 +26,7 @@ binary_sensor:
 
 sensor:
   - platform: binary_sensor_map
-    name: Binary Sensor Map
+    name: Binary Sensor Map Group
     type: group
     channels:
       - binary_sensor: bin1
@@ -36,7 +36,7 @@ sensor:
       - binary_sensor: bin3
         value: 100.0
   - platform: binary_sensor_map
-    name: Binary Sensor Map
+    name: Binary Sensor Map Sum
     type: sum
     channels:
       - binary_sensor: bin1
@@ -46,7 +46,7 @@ sensor:
       - binary_sensor: bin3
         value: 100.0
   - platform: binary_sensor_map
-    name: Binary Sensor Map
+    name: Binary Sensor Map Bayesian
     type: bayesian
     prior: 0.4
     observations:
diff --git a/tests/components/bluetooth_proxy/test.esp32-c3-ard.yaml b/tests/components/bluetooth_proxy/test.esp32-c3-ard.yaml
deleted file mode 100644
index bf01b65b6f..0000000000
--- a/tests/components/bluetooth_proxy/test.esp32-c3-ard.yaml
+++ /dev/null
@@ -1,8 +0,0 @@
-<<: !include common.yaml
-
-esp32_ble_tracker:
-  max_connections: 3
-
-bluetooth_proxy:
-  active: true
-  connection_slots: 2
diff --git a/tests/components/bluetooth_proxy/test.esp32-c3-idf.yaml b/tests/components/bluetooth_proxy/test.esp32-c6-idf.yaml
similarity index 100%
rename from tests/components/bluetooth_proxy/test.esp32-c3-idf.yaml
rename to tests/components/bluetooth_proxy/test.esp32-c6-idf.yaml
diff --git a/tests/components/bluetooth_proxy/test.esp32-ard.yaml b/tests/components/bluetooth_proxy/test.esp32-s3-ard.yaml
similarity index 100%
rename from tests/components/bluetooth_proxy/test.esp32-ard.yaml
rename to tests/components/bluetooth_proxy/test.esp32-s3-ard.yaml
diff --git a/tests/components/bluetooth_proxy/test.esp32-idf.yaml b/tests/components/bluetooth_proxy/test.esp32-s3-idf.yaml
similarity index 100%
rename from tests/components/bluetooth_proxy/test.esp32-idf.yaml
rename to tests/components/bluetooth_proxy/test.esp32-s3-idf.yaml
diff --git a/tests/components/camera/common.yaml b/tests/components/camera/common.yaml
new file mode 100644
index 0000000000..3daf1e8565
--- /dev/null
+++ b/tests/components/camera/common.yaml
@@ -0,0 +1,18 @@
+esphome:
+  includes:
+    - ../../../esphome/components/camera/
+
+script:
+  - id: interface_compile_check
+    then:
+      - lambda: |-
+            using namespace esphome::camera;
+            class MockCamera : public Camera {
+              public:
+                void add_image_callback(std::function)> &&callback) override {}
+                CameraImageReader *create_image_reader() override { return 0; }
+                void request_image(CameraRequester requester) override {}
+                void start_stream(CameraRequester requester) override {}
+                void stop_stream(CameraRequester requester) override {}
+            };
+            MockCamera* camera = new MockCamera();
diff --git a/tests/components/camera/test.esp32-ard.yaml b/tests/components/camera/test.esp32-ard.yaml
new file mode 100644
index 0000000000..dade44d145
--- /dev/null
+++ b/tests/components/camera/test.esp32-ard.yaml
@@ -0,0 +1 @@
+<<: !include common.yaml
diff --git a/tests/components/camera/test.esp32-idf.yaml b/tests/components/camera/test.esp32-idf.yaml
new file mode 100644
index 0000000000..dade44d145
--- /dev/null
+++ b/tests/components/camera/test.esp32-idf.yaml
@@ -0,0 +1 @@
+<<: !include common.yaml
diff --git a/tests/components/dallas_temp/common.yaml b/tests/components/dallas_temp/common.yaml
index 2f846ca278..fb51f4818e 100644
--- a/tests/components/dallas_temp/common.yaml
+++ b/tests/components/dallas_temp/common.yaml
@@ -5,7 +5,7 @@ one_wire:
 sensor:
   - platform: dallas_temp
     address: 0x1C0000031EDD2A28
-    name: Dallas Temperature
+    name: Dallas Temperature 1
     resolution: 9
   - platform: dallas_temp
-    name: Dallas Temperature
+    name: Dallas Temperature 2
diff --git a/tests/components/debug/test.ln882x-ard.yaml b/tests/components/debug/test.ln882x-ard.yaml
new file mode 100644
index 0000000000..dade44d145
--- /dev/null
+++ b/tests/components/debug/test.ln882x-ard.yaml
@@ -0,0 +1 @@
+<<: !include common.yaml
diff --git a/tests/components/ds2484/common.yaml b/tests/components/ds2484/common.yaml
new file mode 100644
index 0000000000..9d2882a3c0
--- /dev/null
+++ b/tests/components/ds2484/common.yaml
@@ -0,0 +1,11 @@
+i2c:
+  - id: i2c_ds2484
+    scl: ${scl_pin}
+    sda: ${sda_pin}
+
+one_wire:
+  platform: ds2484
+  i2c_id: i2c_ds2484
+  address: 0x18
+  active_pullup: true
+  strong_pullup: false
diff --git a/tests/components/ds2484/test.esp32-ard.yaml b/tests/components/ds2484/test.esp32-ard.yaml
new file mode 100644
index 0000000000..63c3bd6afd
--- /dev/null
+++ b/tests/components/ds2484/test.esp32-ard.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  scl_pin: GPIO16
+  sda_pin: GPIO17
+
+<<: !include common.yaml
diff --git a/tests/components/ds2484/test.esp32-c3-ard.yaml b/tests/components/ds2484/test.esp32-c3-ard.yaml
new file mode 100644
index 0000000000..ee2c29ca4e
--- /dev/null
+++ b/tests/components/ds2484/test.esp32-c3-ard.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  scl_pin: GPIO5
+  sda_pin: GPIO4
+
+<<: !include common.yaml
diff --git a/tests/components/ds2484/test.esp32-c3-idf.yaml b/tests/components/ds2484/test.esp32-c3-idf.yaml
new file mode 100644
index 0000000000..ee2c29ca4e
--- /dev/null
+++ b/tests/components/ds2484/test.esp32-c3-idf.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  scl_pin: GPIO5
+  sda_pin: GPIO4
+
+<<: !include common.yaml
diff --git a/tests/components/ds2484/test.esp32-idf.yaml b/tests/components/ds2484/test.esp32-idf.yaml
new file mode 100644
index 0000000000..63c3bd6afd
--- /dev/null
+++ b/tests/components/ds2484/test.esp32-idf.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  scl_pin: GPIO16
+  sda_pin: GPIO17
+
+<<: !include common.yaml
diff --git a/tests/components/ds2484/test.esp8266-ard.yaml b/tests/components/ds2484/test.esp8266-ard.yaml
new file mode 100644
index 0000000000..ee2c29ca4e
--- /dev/null
+++ b/tests/components/ds2484/test.esp8266-ard.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  scl_pin: GPIO5
+  sda_pin: GPIO4
+
+<<: !include common.yaml
diff --git a/tests/components/ds2484/test.rp2040-ard.yaml b/tests/components/ds2484/test.rp2040-ard.yaml
new file mode 100644
index 0000000000..ee2c29ca4e
--- /dev/null
+++ b/tests/components/ds2484/test.rp2040-ard.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  scl_pin: GPIO5
+  sda_pin: GPIO4
+
+<<: !include common.yaml
diff --git a/tests/components/e131/common-ard.yaml b/tests/components/e131/common-ard.yaml
index 418453d6ef..8300dbb01b 100644
--- a/tests/components/e131/common-ard.yaml
+++ b/tests/components/e131/common-ard.yaml
@@ -8,7 +8,6 @@ light:
     rgb_order: GRB
     num_leds: 256
     pin: ${pin}
-    rmt_channel: 0
     effects:
       - e131:
           universe: 1
diff --git a/tests/components/esp32_camera_web_server/common.yaml b/tests/components/esp32_camera_web_server/common.yaml
index 5edefdf0a8..fe2a6a2739 100644
--- a/tests/components/esp32_camera_web_server/common.yaml
+++ b/tests/components/esp32_camera_web_server/common.yaml
@@ -32,3 +32,7 @@ esp32_camera_web_server:
     mode: stream
   - port: 8081
     mode: snapshot
+
+wifi:
+  ssid: MySSID
+  password: password1
diff --git a/tests/components/esp32_hall/test.esp32-ard.yaml b/tests/components/esp32_hall/test.esp32-ard.yaml
deleted file mode 100644
index f8429f5aa0..0000000000
--- a/tests/components/esp32_hall/test.esp32-ard.yaml
+++ /dev/null
@@ -1,3 +0,0 @@
-sensor:
-  - platform: esp32_hall
-    name: ESP32 Hall Sensor
diff --git a/tests/components/esp32_hosted/common.yaml b/tests/components/esp32_hosted/common.yaml
new file mode 100644
index 0000000000..ab029e5064
--- /dev/null
+++ b/tests/components/esp32_hosted/common.yaml
@@ -0,0 +1,15 @@
+esp32_hosted:
+  variant: ESP32C6
+  slot: 1
+  active_high: true
+  reset_pin: GPIO15
+  cmd_pin: GPIO13
+  clk_pin: GPIO12
+  d0_pin: GPIO11
+  d1_pin: GPIO10
+  d2_pin: GPIO9
+  d3_pin: GPIO8
+
+wifi:
+  ssid: MySSID
+  password: password1
diff --git a/tests/components/esp32_hosted/test.esp32-p4-idf.yaml b/tests/components/esp32_hosted/test.esp32-p4-idf.yaml
new file mode 100644
index 0000000000..dade44d145
--- /dev/null
+++ b/tests/components/esp32_hosted/test.esp32-p4-idf.yaml
@@ -0,0 +1 @@
+<<: !include common.yaml
diff --git a/tests/components/esp32_rmt_led_strip/common-ard.yaml b/tests/components/esp32_rmt_led_strip/common-ard.yaml
deleted file mode 100644
index 287690e86e..0000000000
--- a/tests/components/esp32_rmt_led_strip/common-ard.yaml
+++ /dev/null
@@ -1,18 +0,0 @@
-light:
-  - platform: esp32_rmt_led_strip
-    id: led_strip1
-    pin: ${pin1}
-    num_leds: 60
-    rmt_channel: 0
-    rgb_order: GRB
-    chipset: ws2812
-  - platform: esp32_rmt_led_strip
-    id: led_strip2
-    pin: ${pin2}
-    num_leds: 60
-    rmt_channel: 1
-    rgb_order: RGB
-    bit0_high: 100us
-    bit0_low: 100us
-    bit1_high: 100us
-    bit1_low: 100us
diff --git a/tests/components/esp32_rmt_led_strip/common-idf.yaml b/tests/components/esp32_rmt_led_strip/common.yaml
similarity index 100%
rename from tests/components/esp32_rmt_led_strip/common-idf.yaml
rename to tests/components/esp32_rmt_led_strip/common.yaml
diff --git a/tests/components/esp32_rmt_led_strip/test.esp32-ard.yaml b/tests/components/esp32_rmt_led_strip/test.esp32-ard.yaml
index d5a9ec9435..0949b676d5 100644
--- a/tests/components/esp32_rmt_led_strip/test.esp32-ard.yaml
+++ b/tests/components/esp32_rmt_led_strip/test.esp32-ard.yaml
@@ -3,4 +3,4 @@ substitutions:
   pin2: GPIO14
 
 packages:
-  common: !include common-ard.yaml
+  common: !include common.yaml
diff --git a/tests/components/esp32_rmt_led_strip/test.esp32-c3-ard.yaml b/tests/components/esp32_rmt_led_strip/test.esp32-c3-ard.yaml
index 2a3cdec60d..6cc0667e77 100644
--- a/tests/components/esp32_rmt_led_strip/test.esp32-c3-ard.yaml
+++ b/tests/components/esp32_rmt_led_strip/test.esp32-c3-ard.yaml
@@ -3,4 +3,4 @@ substitutions:
   pin2: GPIO4
 
 packages:
-  common: !include common-ard.yaml
+  common: !include common.yaml
diff --git a/tests/components/esp32_rmt_led_strip/test.esp32-c3-idf.yaml b/tests/components/esp32_rmt_led_strip/test.esp32-c3-idf.yaml
index 8feded852c..6cc0667e77 100644
--- a/tests/components/esp32_rmt_led_strip/test.esp32-c3-idf.yaml
+++ b/tests/components/esp32_rmt_led_strip/test.esp32-c3-idf.yaml
@@ -3,4 +3,4 @@ substitutions:
   pin2: GPIO4
 
 packages:
-  common: !include common-idf.yaml
+  common: !include common.yaml
diff --git a/tests/components/esp32_rmt_led_strip/test.esp32-idf.yaml b/tests/components/esp32_rmt_led_strip/test.esp32-idf.yaml
index bb26436e5b..0949b676d5 100644
--- a/tests/components/esp32_rmt_led_strip/test.esp32-idf.yaml
+++ b/tests/components/esp32_rmt_led_strip/test.esp32-idf.yaml
@@ -3,4 +3,4 @@ substitutions:
   pin2: GPIO14
 
 packages:
-  common: !include common-idf.yaml
+  common: !include common.yaml
diff --git a/tests/components/esp32_rmt_led_strip/test.esp32-s3-idf.yaml b/tests/components/esp32_rmt_led_strip/test.esp32-s3-idf.yaml
index f64bb9d8a5..ad273903b2 100644
--- a/tests/components/esp32_rmt_led_strip/test.esp32-s3-idf.yaml
+++ b/tests/components/esp32_rmt_led_strip/test.esp32-s3-idf.yaml
@@ -3,7 +3,7 @@ substitutions:
   pin2: GPIO4
 
 packages:
-  common: !include common-idf.yaml
+  common: !include common.yaml
 
 light:
   - id: !extend led_strip1
diff --git a/tests/components/esphome/common.yaml b/tests/components/esphome/common.yaml
index 05954e37d7..a4b309b69d 100644
--- a/tests/components/esphome/common.yaml
+++ b/tests/components/esphome/common.yaml
@@ -2,7 +2,9 @@ esphome:
   debug_scheduler: true
   platformio_options:
     board_build.flash_mode: dio
-  area: testing
+  area:
+    id: testing_area
+    name: Testing Area
   on_boot:
     logger.log: on_boot
   on_shutdown:
@@ -17,4 +19,20 @@ esphome:
     version: "1.1"
     on_update:
       logger.log: on_update
+  areas:
+    - id: another_area
+      name: Another area
+  devices:
+    - id: other_device
+      name: Another device
+      area_id: another_area
+    - id: test_device
+      name: Test device in main area
+      area_id: testing_area  # Reference the main area (not in areas)
+    - id: no_area_device
+      name: Device without area  # This device has no area_id
 
+binary_sensor:
+  - platform: template
+    name: Other device sensor
+    device_id: other_device
diff --git a/tests/components/ethernet/common-dm9051.yaml b/tests/components/ethernet/common-dm9051.yaml
new file mode 100644
index 0000000000..c878ca6e59
--- /dev/null
+++ b/tests/components/ethernet/common-dm9051.yaml
@@ -0,0 +1,14 @@
+ethernet:
+  type: DM9051
+  clk_pin: 19
+  mosi_pin: 21
+  miso_pin: 23
+  cs_pin: 18
+  interrupt_pin: 36
+  reset_pin: 22
+  clock_speed: 10Mhz
+  manual_ip:
+    static_ip: 192.168.178.56
+    gateway: 192.168.178.1
+    subnet: 255.255.255.0
+  domain: .local
diff --git a/tests/components/ethernet/common-dp83848.yaml b/tests/components/ethernet/common-dp83848.yaml
index 5b6ed3e8d0..140c7d0d1b 100644
--- a/tests/components/ethernet/common-dp83848.yaml
+++ b/tests/components/ethernet/common-dp83848.yaml
@@ -2,7 +2,9 @@ ethernet:
   type: DP83848
   mdc_pin: 23
   mdio_pin: 25
-  clk_mode: GPIO0_IN
+  clk:
+    pin: 0
+    mode: CLK_EXT_IN
   phy_addr: 0
   power_pin: 26
   manual_ip:
diff --git a/tests/components/ethernet/common-ip101.yaml b/tests/components/ethernet/common-ip101.yaml
index 5ca369cce1..b5589220de 100644
--- a/tests/components/ethernet/common-ip101.yaml
+++ b/tests/components/ethernet/common-ip101.yaml
@@ -2,7 +2,9 @@ ethernet:
   type: IP101
   mdc_pin: 23
   mdio_pin: 25
-  clk_mode: GPIO0_IN
+  clk:
+    pin: 0
+    mode: CLK_EXT_IN
   phy_addr: 0
   power_pin: 26
   manual_ip:
diff --git a/tests/components/ethernet/common-jl1101.yaml b/tests/components/ethernet/common-jl1101.yaml
index 639542d807..2ada9495a0 100644
--- a/tests/components/ethernet/common-jl1101.yaml
+++ b/tests/components/ethernet/common-jl1101.yaml
@@ -2,7 +2,9 @@ ethernet:
   type: JL1101
   mdc_pin: 23
   mdio_pin: 25
-  clk_mode: GPIO0_IN
+  clk:
+    pin: 0
+    mode: CLK_EXT_IN
   phy_addr: 0
   power_pin: 26
   manual_ip:
diff --git a/tests/components/ethernet/common-ksz8081.yaml b/tests/components/ethernet/common-ksz8081.yaml
index 167606a1eb..7da8adb09a 100644
--- a/tests/components/ethernet/common-ksz8081.yaml
+++ b/tests/components/ethernet/common-ksz8081.yaml
@@ -2,7 +2,9 @@ ethernet:
   type: KSZ8081
   mdc_pin: 23
   mdio_pin: 25
-  clk_mode: GPIO0_IN
+  clk:
+    pin: 0
+    mode: CLK_EXT_IN
   phy_addr: 0
   power_pin: 26
   manual_ip:
diff --git a/tests/components/ethernet/common-ksz8081rna.yaml b/tests/components/ethernet/common-ksz8081rna.yaml
index f506906b1b..df04f06132 100644
--- a/tests/components/ethernet/common-ksz8081rna.yaml
+++ b/tests/components/ethernet/common-ksz8081rna.yaml
@@ -2,7 +2,9 @@ ethernet:
   type: KSZ8081RNA
   mdc_pin: 23
   mdio_pin: 25
-  clk_mode: GPIO0_IN
+  clk:
+    pin: 0
+    mode: CLK_EXT_IN
   phy_addr: 0
   power_pin: 26
   manual_ip:
diff --git a/tests/components/ethernet/common-lan8720.yaml b/tests/components/ethernet/common-lan8720.yaml
index b9ed9cb036..f227752f42 100644
--- a/tests/components/ethernet/common-lan8720.yaml
+++ b/tests/components/ethernet/common-lan8720.yaml
@@ -2,7 +2,9 @@ ethernet:
   type: LAN8720
   mdc_pin: 23
   mdio_pin: 25
-  clk_mode: GPIO0_IN
+  clk:
+    pin: 0
+    mode: CLK_EXT_IN
   phy_addr: 0
   power_pin: 26
   manual_ip:
diff --git a/tests/components/ethernet/common-rtl8201.yaml b/tests/components/ethernet/common-rtl8201.yaml
index 43842e7c9f..7c9c9d913c 100644
--- a/tests/components/ethernet/common-rtl8201.yaml
+++ b/tests/components/ethernet/common-rtl8201.yaml
@@ -2,7 +2,9 @@ ethernet:
   type: RTL8201
   mdc_pin: 23
   mdio_pin: 25
-  clk_mode: GPIO0_IN
+  clk:
+    pin: 0
+    mode: CLK_EXT_IN
   phy_addr: 0
   power_pin: 26
   manual_ip:
diff --git a/tests/components/ethernet/test-dm9051.esp32-ard.yaml b/tests/components/ethernet/test-dm9051.esp32-ard.yaml
new file mode 100644
index 0000000000..23e3b97740
--- /dev/null
+++ b/tests/components/ethernet/test-dm9051.esp32-ard.yaml
@@ -0,0 +1 @@
+<<: !include common-dm9051.yaml
diff --git a/tests/components/ethernet/test-dm9051.esp32-idf.yaml b/tests/components/ethernet/test-dm9051.esp32-idf.yaml
new file mode 100644
index 0000000000..23e3b97740
--- /dev/null
+++ b/tests/components/ethernet/test-dm9051.esp32-idf.yaml
@@ -0,0 +1 @@
+<<: !include common-dm9051.yaml
diff --git a/tests/components/ethernet_info/common.yaml b/tests/components/ethernet_info/common.yaml
index d9a6f515b1..f45f345316 100644
--- a/tests/components/ethernet_info/common.yaml
+++ b/tests/components/ethernet_info/common.yaml
@@ -2,7 +2,9 @@ ethernet:
   type: LAN8720
   mdc_pin: 23
   mdio_pin: 25
-  clk_mode: GPIO0_IN
+  clk:
+    pin: 0
+    mode: CLK_EXT_IN
   phy_addr: 0
   power_pin: 26
   manual_ip:
diff --git a/tests/components/gl_r01_i2c/common.yaml b/tests/components/gl_r01_i2c/common.yaml
new file mode 100644
index 0000000000..fe0705bdc6
--- /dev/null
+++ b/tests/components/gl_r01_i2c/common.yaml
@@ -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
diff --git a/tests/components/gl_r01_i2c/test.esp32-ard.yaml b/tests/components/gl_r01_i2c/test.esp32-ard.yaml
new file mode 100644
index 0000000000..63c3bd6afd
--- /dev/null
+++ b/tests/components/gl_r01_i2c/test.esp32-ard.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  scl_pin: GPIO16
+  sda_pin: GPIO17
+
+<<: !include common.yaml
diff --git a/tests/components/gl_r01_i2c/test.esp32-c3-ard.yaml b/tests/components/gl_r01_i2c/test.esp32-c3-ard.yaml
new file mode 100644
index 0000000000..ee2c29ca4e
--- /dev/null
+++ b/tests/components/gl_r01_i2c/test.esp32-c3-ard.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  scl_pin: GPIO5
+  sda_pin: GPIO4
+
+<<: !include common.yaml
diff --git a/tests/components/gl_r01_i2c/test.esp32-c3-idf.yaml b/tests/components/gl_r01_i2c/test.esp32-c3-idf.yaml
new file mode 100644
index 0000000000..ee2c29ca4e
--- /dev/null
+++ b/tests/components/gl_r01_i2c/test.esp32-c3-idf.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  scl_pin: GPIO5
+  sda_pin: GPIO4
+
+<<: !include common.yaml
diff --git a/tests/components/gl_r01_i2c/test.esp32-idf.yaml b/tests/components/gl_r01_i2c/test.esp32-idf.yaml
new file mode 100644
index 0000000000..63c3bd6afd
--- /dev/null
+++ b/tests/components/gl_r01_i2c/test.esp32-idf.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  scl_pin: GPIO16
+  sda_pin: GPIO17
+
+<<: !include common.yaml
diff --git a/tests/components/gl_r01_i2c/test.esp8266-ard.yaml b/tests/components/gl_r01_i2c/test.esp8266-ard.yaml
new file mode 100644
index 0000000000..ee2c29ca4e
--- /dev/null
+++ b/tests/components/gl_r01_i2c/test.esp8266-ard.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  scl_pin: GPIO5
+  sda_pin: GPIO4
+
+<<: !include common.yaml
diff --git a/tests/components/gl_r01_i2c/test.rp2040-ard.yaml b/tests/components/gl_r01_i2c/test.rp2040-ard.yaml
new file mode 100644
index 0000000000..ee2c29ca4e
--- /dev/null
+++ b/tests/components/gl_r01_i2c/test.rp2040-ard.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  scl_pin: GPIO5
+  sda_pin: GPIO4
+
+<<: !include common.yaml
diff --git a/tests/components/heatpumpir/common.yaml b/tests/components/heatpumpir/common.yaml
index 2df195c5de..d740f31518 100644
--- a/tests/components/heatpumpir/common.yaml
+++ b/tests/components/heatpumpir/common.yaml
@@ -7,20 +7,20 @@ climate:
     protocol: mitsubishi_heavy_zm
     horizontal_default: left
     vertical_default: up
-    name: HeatpumpIR Climate
+    name: HeatpumpIR Climate Mitsubishi
     min_temperature: 18
     max_temperature: 30
   - platform: heatpumpir
     protocol: daikin
     horizontal_default: mleft
     vertical_default: mup
-    name: HeatpumpIR Climate
+    name: HeatpumpIR Climate Daikin
     min_temperature: 18
     max_temperature: 30
   - platform: heatpumpir
     protocol: panasonic_altdke
     horizontal_default: mright
     vertical_default: mdown
-    name: HeatpumpIR Climate
+    name: HeatpumpIR Climate Panasonic
     min_temperature: 18
     max_temperature: 30
diff --git a/tests/components/homeassistant/test.ln882x-ard.yaml b/tests/components/homeassistant/test.ln882x-ard.yaml
new file mode 100644
index 0000000000..25cb37a0b4
--- /dev/null
+++ b/tests/components/homeassistant/test.ln882x-ard.yaml
@@ -0,0 +1,2 @@
+packages:
+  common: !include common.yaml
diff --git a/tests/components/http_request/common.yaml b/tests/components/http_request/common.yaml
index af4852901f..97961007e2 100644
--- a/tests/components/http_request/common.yaml
+++ b/tests/components/http_request/common.yaml
@@ -91,3 +91,5 @@ update:
     name: OTA Update
     id: ota_update
     source: http://my.ha.net:8123/local/esphome/manifest.json
+    on_update_available:
+      - logger.log: "A new update is available"
diff --git a/tests/components/image/test.esp32-ard.yaml b/tests/components/image/test.esp32-ard.yaml
deleted file mode 100644
index 818e720221..0000000000
--- a/tests/components/image/test.esp32-ard.yaml
+++ /dev/null
@@ -1,17 +0,0 @@
-spi:
-  - id: spi_main_lcd
-    clk_pin: 16
-    mosi_pin: 17
-    miso_pin: 32
-
-display:
-  - platform: ili9xxx
-    id: main_lcd
-    model: ili9342
-    cs_pin: 14
-    dc_pin: 13
-    reset_pin: 21
-    invert_colors: true
-
-<<: !include common.yaml
-
diff --git a/tests/components/image/test.esp32-c3-ard.yaml b/tests/components/image/test.esp32-c3-ard.yaml
deleted file mode 100644
index 4dae9cd5ec..0000000000
--- a/tests/components/image/test.esp32-c3-ard.yaml
+++ /dev/null
@@ -1,16 +0,0 @@
-spi:
-  - id: spi_main_lcd
-    clk_pin: 6
-    mosi_pin: 7
-    miso_pin: 5
-
-display:
-  - platform: ili9xxx
-    id: main_lcd
-    model: ili9342
-    cs_pin: 3
-    dc_pin: 11
-    reset_pin: 10
-    invert_colors: true
-
-<<: !include common.yaml
diff --git a/tests/components/image/test.esp32-c3-idf.yaml b/tests/components/image/test.esp32-c3-idf.yaml
deleted file mode 100644
index 4dae9cd5ec..0000000000
--- a/tests/components/image/test.esp32-c3-idf.yaml
+++ /dev/null
@@ -1,16 +0,0 @@
-spi:
-  - id: spi_main_lcd
-    clk_pin: 6
-    mosi_pin: 7
-    miso_pin: 5
-
-display:
-  - platform: ili9xxx
-    id: main_lcd
-    model: ili9342
-    cs_pin: 3
-    dc_pin: 11
-    reset_pin: 10
-    invert_colors: true
-
-<<: !include common.yaml
diff --git a/tests/components/image/test.esp8266-ard.yaml b/tests/components/image/test.esp8266-ard.yaml
index f963022ff4..626076d44e 100644
--- a/tests/components/image/test.esp8266-ard.yaml
+++ b/tests/components/image/test.esp8266-ard.yaml
@@ -13,4 +13,13 @@ display:
     reset_pin: 16
     invert_colors: true
 
-<<: !include common.yaml
+image:
+  defaults:
+    type: rgb565
+    transparency: opaque
+    byte_order: little_endian
+    resize: 50x50
+    dither: FloydSteinberg
+  images:
+    - id: test_image
+      file: ../../pnglogo.png
diff --git a/tests/components/inkplate6/common.yaml b/tests/components/inkplate6/common.yaml
index 31b14e6c73..6cb5d055b6 100644
--- a/tests/components/inkplate6/common.yaml
+++ b/tests/components/inkplate6/common.yaml
@@ -3,6 +3,9 @@ i2c:
     scl: 16
     sda: 17
 
+esp32:
+  cpu_frequency: 240MHz
+
 display:
   - platform: inkplate6
     id: inkplate_display
diff --git a/tests/components/internal_temperature/test.esp32-s3-ard.yaml b/tests/components/internal_temperature/test.esp32-s3-ard.yaml
index bdd704756c..dade44d145 100644
--- a/tests/components/internal_temperature/test.esp32-s3-ard.yaml
+++ b/tests/components/internal_temperature/test.esp32-s3-ard.yaml
@@ -1,5 +1 @@
 <<: !include common.yaml
-
-esp32:
-  framework:
-    version: 2.0.9
diff --git a/tests/components/light/common.yaml b/tests/components/light/common.yaml
index a224dbe8bc..d4f64dcdea 100644
--- a/tests/components/light/common.yaml
+++ b/tests/components/light/common.yaml
@@ -114,7 +114,7 @@ light:
     warm_white_color_temperature: 500 mireds
   - platform: rgb
     id: test_rgb_light_initial_state
-    name: RGB Light
+    name: RGB Light Initial State
     red: test_ledc_1
     green: test_ledc_2
     blue: test_ledc_3
diff --git a/tests/components/lps22/common.yaml b/tests/components/lps22/common.yaml
new file mode 100644
index 0000000000..e6de4752ba
--- /dev/null
+++ b/tests/components/lps22/common.yaml
@@ -0,0 +1,8 @@
+sensor:
+  - platform: lps22
+    address: 0x5d
+    update_interval: 10s
+    temperature:
+      name: "LPS22 Temperature"
+    pressure:
+      name: "LPS22 Pressure"
diff --git a/tests/components/lps22/test.esp32-ard.yaml b/tests/components/lps22/test.esp32-ard.yaml
new file mode 100644
index 0000000000..0da6a9577e
--- /dev/null
+++ b/tests/components/lps22/test.esp32-ard.yaml
@@ -0,0 +1,6 @@
+i2c:
+  - id: i2c_lps22
+    scl: 16
+    sda: 17
+
+<<: !include common.yaml
diff --git a/tests/components/lps22/test.esp32-c3-ard.yaml b/tests/components/lps22/test.esp32-c3-ard.yaml
new file mode 100644
index 0000000000..6091393d31
--- /dev/null
+++ b/tests/components/lps22/test.esp32-c3-ard.yaml
@@ -0,0 +1,6 @@
+i2c:
+  - id: i2c_lps22
+    scl: 5
+    sda: 4
+
+<<: !include common.yaml
diff --git a/tests/components/lps22/test.esp32-c3-idf.yaml b/tests/components/lps22/test.esp32-c3-idf.yaml
new file mode 100644
index 0000000000..6091393d31
--- /dev/null
+++ b/tests/components/lps22/test.esp32-c3-idf.yaml
@@ -0,0 +1,6 @@
+i2c:
+  - id: i2c_lps22
+    scl: 5
+    sda: 4
+
+<<: !include common.yaml
diff --git a/tests/components/lps22/test.esp32-idf.yaml b/tests/components/lps22/test.esp32-idf.yaml
new file mode 100644
index 0000000000..0da6a9577e
--- /dev/null
+++ b/tests/components/lps22/test.esp32-idf.yaml
@@ -0,0 +1,6 @@
+i2c:
+  - id: i2c_lps22
+    scl: 16
+    sda: 17
+
+<<: !include common.yaml
diff --git a/tests/components/lps22/test.esp8266-ard.yaml b/tests/components/lps22/test.esp8266-ard.yaml
new file mode 100644
index 0000000000..6091393d31
--- /dev/null
+++ b/tests/components/lps22/test.esp8266-ard.yaml
@@ -0,0 +1,6 @@
+i2c:
+  - id: i2c_lps22
+    scl: 5
+    sda: 4
+
+<<: !include common.yaml
diff --git a/tests/components/lps22/test.rp2040-ard.yaml b/tests/components/lps22/test.rp2040-ard.yaml
new file mode 100644
index 0000000000..6091393d31
--- /dev/null
+++ b/tests/components/lps22/test.rp2040-ard.yaml
@@ -0,0 +1,6 @@
+i2c:
+  - id: i2c_lps22
+    scl: 5
+    sda: 4
+
+<<: !include common.yaml
diff --git a/tests/components/ltr390/common.yaml b/tests/components/ltr390/common.yaml
index 2eebe9d1c3..e5e331e7ba 100644
--- a/tests/components/ltr390/common.yaml
+++ b/tests/components/ltr390/common.yaml
@@ -6,13 +6,13 @@ i2c:
 sensor:
   - platform: ltr390
     uv:
-      name: LTR390 UV
+      name: LTR390 UV 1
     uv_index:
-      name: LTR390 UVI
+      name: LTR390 UVI 1
     light:
-      name: LTR390 Light
+      name: LTR390 Light 1
     ambient_light:
-      name: LTR390 ALS
+      name: LTR390 ALS 1
     gain: X3
     resolution: 18
     window_correction_factor: 1.0
@@ -20,13 +20,13 @@ sensor:
     update_interval: 60s
   - platform: ltr390
     uv:
-      name: LTR390 UV
+      name: LTR390 UV 2
     uv_index:
-      name: LTR390 UVI
+      name: LTR390 UVI 2
     light:
-      name: LTR390 Light
+      name: LTR390 Light 2
     ambient_light:
-      name: LTR390 ALS
+      name: LTR390 ALS 2
     gain:
       ambient_light: X9
       uv: X3
diff --git a/tests/components/lvgl/common.yaml b/tests/components/lvgl/common.yaml
index 174df56749..a035900386 100644
--- a/tests/components/lvgl/common.yaml
+++ b/tests/components/lvgl/common.yaml
@@ -24,33 +24,33 @@ sensor:
     widget: lv_arc
   - platform: lvgl
     widget: slider_id
-    name: LVGL Slider
+    name: LVGL Slider Sensor
   - platform: lvgl
     widget: bar_id
     id: lvgl_bar_sensor
-    name: LVGL Bar
+    name: LVGL Bar Sensor
   - platform: lvgl
     widget: spinbox_id
-    name: LVGL Spinbox
+    name: LVGL Spinbox Sensor
 
 number:
   - platform: lvgl
     widget: slider_id
-    name: LVGL Slider
+    name: LVGL Slider Number
     update_on_release: true
     restore_value: true
   - platform: lvgl
     widget: lv_arc
     id: lvgl_arc_number
-    name: LVGL Arc
+    name: LVGL Arc Number
   - platform: lvgl
     widget: bar_id
     id: lvgl_bar_number
-    name: LVGL Bar
+    name: LVGL Bar Number
   - platform: lvgl
     widget: spinbox_id
     id: lvgl_spinbox_number
-    name: LVGL Spinbox
+    name: LVGL Spinbox Number
 
 light:
   - platform: lvgl
@@ -63,7 +63,7 @@ binary_sensor:
     id: lvgl_pressbutton
     name: Pressbutton
     widget: spin_up
-    publish_initial_state: true
+    trigger_on_initial_state: true
   - platform: lvgl
     name: ButtonMatrix button
     widget: button_a
diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml
index e3391a5bac..2edc62b6a1 100644
--- a/tests/components/lvgl/lvgl-package.yaml
+++ b/tests/components/lvgl/lvgl-package.yaml
@@ -730,12 +730,15 @@ lvgl:
             value: 30
             max_value: 100
             min_value: 10
+            start_value: 20
             mode: range
             on_click:
               then:
                 - lvgl.bar.update:
                     id: bar_id
                     value: !lambda return (int)((float)rand() / RAND_MAX * 100);
+                    start_value: !lambda return (int)((float)rand() / RAND_MAX * 100);
+                    mode: symmetrical
                 - logger.log:
                     format: "bar value %f"
                     args: [x]
@@ -836,9 +839,7 @@ lvgl:
                       styles: bdr_style
                       grid_cell_x_align: center
                       grid_cell_y_align: stretch
-                      grid_cell_row_pos: 0
-                      grid_cell_column_pos: 1
-                      grid_cell_column_span: 1
+                      grid_cell_column_span: 2
                       text: "Grid cell 0/1"
                   - label:
                       grid_cell_x_align: end
diff --git a/tests/components/modbus_controller/common.yaml b/tests/components/modbus_controller/common.yaml
index 7fa9f8dae3..7d342ee353 100644
--- a/tests/components/modbus_controller/common.yaml
+++ b/tests/components/modbus_controller/common.yaml
@@ -33,7 +33,18 @@ modbus_controller:
         read_lambda: |-
           return 42.3;
     max_cmd_retries: 0
-
+  - id: modbus_controller3
+    address: 0x3
+    modbus_id: mod_bus2
+    server_registers:
+      - address: 0x0009
+        value_type: S_DWORD
+        read_lambda: |-
+          return 31;
+        write_lambda: |-
+          printf("address=%d, value=%d", x);
+          return true;
+    max_cmd_retries: 0
 binary_sensor:
   - platform: modbus_controller
     modbus_controller_id: modbus_controller1
diff --git a/tests/components/opentherm/common.yaml b/tests/components/opentherm/common.yaml
index 5edacc6f17..1e58a04bf0 100644
--- a/tests/components/opentherm/common.yaml
+++ b/tests/components/opentherm/common.yaml
@@ -170,4 +170,4 @@ switch:
     otc_active:
       name: "Boiler Outside temperature compensation active"
     ch2_active:
-      name: "Boiler Central Heating 2 active"
+      name: "Boiler Central Heating 2 active status"
diff --git a/tests/components/openthread/test.esp32-c6-idf.yaml b/tests/components/openthread/test.esp32-c6-idf.yaml
index f53b323bec..bbcf48efa5 100644
--- a/tests/components/openthread/test.esp32-c6-idf.yaml
+++ b/tests/components/openthread/test.esp32-c6-idf.yaml
@@ -2,6 +2,7 @@ network:
   enable_ipv6: true
 
 openthread:
+  device_type: FTD
   channel: 13
   network_name: OpenThread-8f28
   network_key: 0xdfd34f0f05cad978ec4e32b0413038ff
diff --git a/tests/components/opt3001/common.yaml b/tests/components/opt3001/common.yaml
new file mode 100644
index 0000000000..dab4f824f8
--- /dev/null
+++ b/tests/components/opt3001/common.yaml
@@ -0,0 +1,10 @@
+i2c:
+  - id: i2c_opt3001
+    scl: ${scl_pin}
+    sda: ${sda_pin}
+
+sensor:
+  - platform: opt3001
+    name: Living Room Brightness
+    address: 0x44
+    update_interval: 30s
diff --git a/tests/components/opt3001/test.esp32-ard.yaml b/tests/components/opt3001/test.esp32-ard.yaml
new file mode 100644
index 0000000000..63c3bd6afd
--- /dev/null
+++ b/tests/components/opt3001/test.esp32-ard.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  scl_pin: GPIO16
+  sda_pin: GPIO17
+
+<<: !include common.yaml
diff --git a/tests/components/opt3001/test.esp32-c3-ard.yaml b/tests/components/opt3001/test.esp32-c3-ard.yaml
new file mode 100644
index 0000000000..ee2c29ca4e
--- /dev/null
+++ b/tests/components/opt3001/test.esp32-c3-ard.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  scl_pin: GPIO5
+  sda_pin: GPIO4
+
+<<: !include common.yaml
diff --git a/tests/components/opt3001/test.esp32-c3-idf.yaml b/tests/components/opt3001/test.esp32-c3-idf.yaml
new file mode 100644
index 0000000000..ee2c29ca4e
--- /dev/null
+++ b/tests/components/opt3001/test.esp32-c3-idf.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  scl_pin: GPIO5
+  sda_pin: GPIO4
+
+<<: !include common.yaml
diff --git a/tests/components/opt3001/test.esp32-idf.yaml b/tests/components/opt3001/test.esp32-idf.yaml
new file mode 100644
index 0000000000..63c3bd6afd
--- /dev/null
+++ b/tests/components/opt3001/test.esp32-idf.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  scl_pin: GPIO16
+  sda_pin: GPIO17
+
+<<: !include common.yaml
diff --git a/tests/components/opt3001/test.esp8266-ard.yaml b/tests/components/opt3001/test.esp8266-ard.yaml
new file mode 100644
index 0000000000..ee2c29ca4e
--- /dev/null
+++ b/tests/components/opt3001/test.esp8266-ard.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  scl_pin: GPIO5
+  sda_pin: GPIO4
+
+<<: !include common.yaml
diff --git a/tests/components/opt3001/test.rp2040-ard.yaml b/tests/components/opt3001/test.rp2040-ard.yaml
new file mode 100644
index 0000000000..ee2c29ca4e
--- /dev/null
+++ b/tests/components/opt3001/test.rp2040-ard.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  scl_pin: GPIO5
+  sda_pin: GPIO4
+
+<<: !include common.yaml
diff --git a/tests/components/packages/test.esp32-ard.yaml b/tests/components/packages/test.esp32-ard.yaml
index d35c27d997..7d0ab2b905 100644
--- a/tests/components/packages/test.esp32-ard.yaml
+++ b/tests/components/packages/test.esp32-ard.yaml
@@ -5,7 +5,8 @@ packages:
   - !include package.yaml
   - github://esphome/esphome/tests/components/template/common.yaml@dev
   - url: https://github.com/esphome/esphome
-    file: tests/components/binary_sensor_map/common.yaml
+    path: tests/components/absolute_humidity
+    file: common.yaml
     ref: dev
     refresh: 1d
 
diff --git a/tests/components/packages/test.esp32-idf.yaml b/tests/components/packages/test.esp32-idf.yaml
index 9f1484d1fd..8c0a34bb1a 100644
--- a/tests/components/packages/test.esp32-idf.yaml
+++ b/tests/components/packages/test.esp32-idf.yaml
@@ -7,7 +7,8 @@ packages:
   shorthand: github://esphome/esphome/tests/components/template/common.yaml@dev
   github:
     url: https://github.com/esphome/esphome
-    file: tests/components/binary_sensor_map/common.yaml
+    path: tests/components/absolute_humidity
+    file: common.yaml
     ref: dev
     refresh: 1d
 
diff --git a/tests/components/packet_transport/common.yaml b/tests/components/packet_transport/common.yaml
index cbb34c4572..9151cf27dc 100644
--- a/tests/components/packet_transport/common.yaml
+++ b/tests/components/packet_transport/common.yaml
@@ -36,5 +36,9 @@ binary_sensor:
   - platform: packet_transport
     provider: unencrypted-device
     id: other_binary_sensor_id
+  - platform: packet_transport
+    provider: some-device-name
+    type: status
+    name: Some-Device Status
   - platform: template
     id: binary_sensor_id1
diff --git a/tests/components/partition/common-ard.yaml b/tests/components/partition/common-ard.yaml
index 654eacf54f..b2ceadd6f7 100644
--- a/tests/components/partition/common-ard.yaml
+++ b/tests/components/partition/common-ard.yaml
@@ -5,7 +5,6 @@ light:
     chipset: ws2812
     num_leds: 256
     rgb_order: GRB
-    rmt_channel: 1
     pin: ${pin}
   - platform: partition
     name: Partition Light
diff --git a/tests/components/pi4ioe5v6408/common.yaml b/tests/components/pi4ioe5v6408/common.yaml
new file mode 100644
index 0000000000..4130dc2652
--- /dev/null
+++ b/tests/components/pi4ioe5v6408/common.yaml
@@ -0,0 +1,22 @@
+i2c:
+  id: i2c_pi4ioe5v6408
+  sda: ${i2c_sda}
+  scl: ${i2c_scl}
+
+pi4ioe5v6408:
+  id: pi4ioe1
+  address: 0x44
+
+switch:
+  - platform: gpio
+    id: switch1
+    pin:
+      pi4ioe5v6408: pi4ioe1
+      number: 0
+
+binary_sensor:
+  - platform: gpio
+    id: sensor1
+    pin:
+      pi4ioe5v6408: pi4ioe1
+      number: 1
diff --git a/tests/components/pi4ioe5v6408/test.esp32-ard.yaml b/tests/components/pi4ioe5v6408/test.esp32-ard.yaml
new file mode 100644
index 0000000000..55e6edfbf3
--- /dev/null
+++ b/tests/components/pi4ioe5v6408/test.esp32-ard.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  i2c_sda: GPIO21
+  i2c_scl: GPIO22
+
+<<: !include common.yaml
diff --git a/tests/components/pi4ioe5v6408/test.esp32-idf.yaml b/tests/components/pi4ioe5v6408/test.esp32-idf.yaml
new file mode 100644
index 0000000000..55e6edfbf3
--- /dev/null
+++ b/tests/components/pi4ioe5v6408/test.esp32-idf.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  i2c_sda: GPIO21
+  i2c_scl: GPIO22
+
+<<: !include common.yaml
diff --git a/tests/components/pi4ioe5v6408/test.rp2040-ard.yaml b/tests/components/pi4ioe5v6408/test.rp2040-ard.yaml
new file mode 100644
index 0000000000..b7b6b13bfe
--- /dev/null
+++ b/tests/components/pi4ioe5v6408/test.rp2040-ard.yaml
@@ -0,0 +1,5 @@
+substitutions:
+  i2c_sda: GPIO4
+  i2c_scl: GPIO5
+
+<<: !include common.yaml
diff --git a/tests/components/remote_receiver/esp32-common-ard.yaml b/tests/components/remote_receiver/esp32-common-ard.yaml
deleted file mode 100644
index e331a35307..0000000000
--- a/tests/components/remote_receiver/esp32-common-ard.yaml
+++ /dev/null
@@ -1,14 +0,0 @@
-remote_receiver:
-  - id: rcvr
-    pin: ${pin}
-    rmt_channel: ${rmt_channel}
-    dump: all
-    tolerance: 25%
-    <<: !include common-actions.yaml
-
-binary_sensor:
-  - platform: remote_receiver
-    name: Panasonic Remote Input
-    panasonic:
-      address: 0x4004
-      command: 0x100BCBD
diff --git a/tests/components/remote_receiver/esp32-common-idf.yaml b/tests/components/remote_receiver/esp32-common.yaml
similarity index 100%
rename from tests/components/remote_receiver/esp32-common-idf.yaml
rename to tests/components/remote_receiver/esp32-common.yaml
diff --git a/tests/components/remote_receiver/test.esp32-ard.yaml b/tests/components/remote_receiver/test.esp32-ard.yaml
index 5d29187206..10dd767598 100644
--- a/tests/components/remote_receiver/test.esp32-ard.yaml
+++ b/tests/components/remote_receiver/test.esp32-ard.yaml
@@ -1,6 +1,9 @@
 substitutions:
   pin: GPIO2
-  rmt_channel: "2"
+  clock_resolution: "2000000"
+  filter_symbols: "2"
+  receive_symbols: "4"
+  rmt_symbols: "64"
 
 packages:
-  common: !include esp32-common-ard.yaml
+  common: !include esp32-common.yaml
diff --git a/tests/components/remote_receiver/test.esp32-c3-ard.yaml b/tests/components/remote_receiver/test.esp32-c3-ard.yaml
index 5d29187206..10dd767598 100644
--- a/tests/components/remote_receiver/test.esp32-c3-ard.yaml
+++ b/tests/components/remote_receiver/test.esp32-c3-ard.yaml
@@ -1,6 +1,9 @@
 substitutions:
   pin: GPIO2
-  rmt_channel: "2"
+  clock_resolution: "2000000"
+  filter_symbols: "2"
+  receive_symbols: "4"
+  rmt_symbols: "64"
 
 packages:
-  common: !include esp32-common-ard.yaml
+  common: !include esp32-common.yaml
diff --git a/tests/components/remote_receiver/test.esp32-c3-idf.yaml b/tests/components/remote_receiver/test.esp32-c3-idf.yaml
index f017a2d807..10dd767598 100644
--- a/tests/components/remote_receiver/test.esp32-c3-idf.yaml
+++ b/tests/components/remote_receiver/test.esp32-c3-idf.yaml
@@ -6,4 +6,4 @@ substitutions:
   rmt_symbols: "64"
 
 packages:
-  common: !include esp32-common-idf.yaml
+  common: !include esp32-common.yaml
diff --git a/tests/components/remote_receiver/test.esp32-idf.yaml b/tests/components/remote_receiver/test.esp32-idf.yaml
index f017a2d807..10dd767598 100644
--- a/tests/components/remote_receiver/test.esp32-idf.yaml
+++ b/tests/components/remote_receiver/test.esp32-idf.yaml
@@ -6,4 +6,4 @@ substitutions:
   rmt_symbols: "64"
 
 packages:
-  common: !include esp32-common-idf.yaml
+  common: !include esp32-common.yaml
diff --git a/tests/components/remote_receiver/test.esp32-s3-idf.yaml b/tests/components/remote_receiver/test.esp32-s3-idf.yaml
index 74f49866cd..cdae8b1e4e 100644
--- a/tests/components/remote_receiver/test.esp32-s3-idf.yaml
+++ b/tests/components/remote_receiver/test.esp32-s3-idf.yaml
@@ -6,7 +6,7 @@ substitutions:
   rmt_symbols: "64"
 
 packages:
-  common: !include esp32-common-idf.yaml
+  common: !include esp32-common.yaml
 
 remote_receiver:
   - id: !extend rcvr
diff --git a/tests/components/remote_transmitter/common-buttons.yaml b/tests/components/remote_transmitter/common-buttons.yaml
index 1fb7ef6dbe..29f48d995d 100644
--- a/tests/components/remote_transmitter/common-buttons.yaml
+++ b/tests/components/remote_transmitter/common-buttons.yaml
@@ -115,7 +115,7 @@ button:
         address: 0x00
         command: 0x0B
   - platform: template
-    name: RC5
+    name: RC5 Raw
     on_press:
       remote_transmitter.transmit_raw:
         code: [1000, -1000]
diff --git a/tests/components/remote_transmitter/esp32-common-ard.yaml b/tests/components/remote_transmitter/esp32-common-ard.yaml
deleted file mode 100644
index 420cea326d..0000000000
--- a/tests/components/remote_transmitter/esp32-common-ard.yaml
+++ /dev/null
@@ -1,8 +0,0 @@
-remote_transmitter:
-  - id: xmitr
-    pin: ${pin}
-    rmt_channel: ${rmt_channel}
-    carrier_duty_percent: 50%
-
-packages:
-  buttons: !include common-buttons.yaml
diff --git a/tests/components/remote_transmitter/esp32-common-idf.yaml b/tests/components/remote_transmitter/esp32-common.yaml
similarity index 100%
rename from tests/components/remote_transmitter/esp32-common-idf.yaml
rename to tests/components/remote_transmitter/esp32-common.yaml
diff --git a/tests/components/remote_transmitter/test.esp32-ard.yaml b/tests/components/remote_transmitter/test.esp32-ard.yaml
index 5d29187206..0522f4d181 100644
--- a/tests/components/remote_transmitter/test.esp32-ard.yaml
+++ b/tests/components/remote_transmitter/test.esp32-ard.yaml
@@ -1,6 +1,7 @@
 substitutions:
   pin: GPIO2
-  rmt_channel: "2"
+  clock_resolution: "2000000"
+  rmt_symbols: "64"
 
 packages:
-  common: !include esp32-common-ard.yaml
+  common: !include esp32-common.yaml
diff --git a/tests/components/remote_transmitter/test.esp32-c3-ard.yaml b/tests/components/remote_transmitter/test.esp32-c3-ard.yaml
index c755b11563..0522f4d181 100644
--- a/tests/components/remote_transmitter/test.esp32-c3-ard.yaml
+++ b/tests/components/remote_transmitter/test.esp32-c3-ard.yaml
@@ -1,6 +1,7 @@
 substitutions:
   pin: GPIO2
-  rmt_channel: "1"
+  clock_resolution: "2000000"
+  rmt_symbols: "64"
 
 packages:
-  common: !include esp32-common-ard.yaml
+  common: !include esp32-common.yaml
diff --git a/tests/components/remote_transmitter/test.esp32-c3-idf.yaml b/tests/components/remote_transmitter/test.esp32-c3-idf.yaml
index cc1fe69b4d..0522f4d181 100644
--- a/tests/components/remote_transmitter/test.esp32-c3-idf.yaml
+++ b/tests/components/remote_transmitter/test.esp32-c3-idf.yaml
@@ -4,4 +4,4 @@ substitutions:
   rmt_symbols: "64"
 
 packages:
-  common: !include esp32-common-idf.yaml
+  common: !include esp32-common.yaml
diff --git a/tests/components/remote_transmitter/test.esp32-idf.yaml b/tests/components/remote_transmitter/test.esp32-idf.yaml
index cc1fe69b4d..0522f4d181 100644
--- a/tests/components/remote_transmitter/test.esp32-idf.yaml
+++ b/tests/components/remote_transmitter/test.esp32-idf.yaml
@@ -4,4 +4,4 @@ substitutions:
   rmt_symbols: "64"
 
 packages:
-  common: !include esp32-common-idf.yaml
+  common: !include esp32-common.yaml
diff --git a/tests/components/remote_transmitter/test.esp32-s3-idf.yaml b/tests/components/remote_transmitter/test.esp32-s3-idf.yaml
index d23463b531..fe4c46d9e7 100644
--- a/tests/components/remote_transmitter/test.esp32-s3-idf.yaml
+++ b/tests/components/remote_transmitter/test.esp32-s3-idf.yaml
@@ -4,7 +4,7 @@ substitutions:
   rmt_symbols: "64"
 
 packages:
-  common: !include esp32-common-idf.yaml
+  common: !include esp32-common.yaml
 
 remote_transmitter:
   - id: !extend xmitr
diff --git a/tests/components/script/test.ln882x-ard.yaml b/tests/components/script/test.ln882x-ard.yaml
new file mode 100644
index 0000000000..dade44d145
--- /dev/null
+++ b/tests/components/script/test.ln882x-ard.yaml
@@ -0,0 +1 @@
+<<: !include common.yaml
diff --git a/tests/components/smt100/common.yaml b/tests/components/smt100/common.yaml
index f86bd762e7..b12d7198fd 100644
--- a/tests/components/smt100/common.yaml
+++ b/tests/components/smt100/common.yaml
@@ -8,8 +8,8 @@ sensor:
   - platform: smt100
     counts:
       name: Counts
-    dielectric_constant:
-      name: Dielectric Constant
+    permittivity:
+      name: Permittivity
     temperature:
       name: Temperature
     moisture:
diff --git a/tests/components/sntp/test.ln882x-ard.yaml b/tests/components/sntp/test.ln882x-ard.yaml
new file mode 100644
index 0000000000..dade44d145
--- /dev/null
+++ b/tests/components/sntp/test.ln882x-ard.yaml
@@ -0,0 +1 @@
+<<: !include common.yaml
diff --git a/tests/components/spi_device/common.yaml b/tests/components/spi_device/common.yaml
index 636d82202b..0f6a5038fb 100644
--- a/tests/components/spi_device/common.yaml
+++ b/tests/components/spi_device/common.yaml
@@ -5,7 +5,7 @@ spi:
     miso_pin: ${miso_pin}
 
 spi_device:
-  id: spi_device_test
-  data_rate: 2MHz
-  spi_mode: 3
-  bit_order: lsb_first
+  - id: spi_device_test
+    data_rate: 2MHz
+    spi_mode: 3
+    bit_order: lsb_first
diff --git a/tests/components/spi_device/test.esp32-idf.yaml b/tests/components/spi_device/test.esp32-idf.yaml
index 448e54fea6..c4989cccbf 100644
--- a/tests/components/spi_device/test.esp32-idf.yaml
+++ b/tests/components/spi_device/test.esp32-idf.yaml
@@ -4,3 +4,8 @@ substitutions:
   miso_pin: GPIO15
 
 <<: !include common.yaml
+spi_device:
+  - id: spi_device_test
+    release_device: true
+    data_rate: 1MHz
+    spi_mode: 0
diff --git a/tests/components/switch/test.ln882x-ard.yaml b/tests/components/switch/test.ln882x-ard.yaml
new file mode 100644
index 0000000000..25cb37a0b4
--- /dev/null
+++ b/tests/components/switch/test.ln882x-ard.yaml
@@ -0,0 +1,2 @@
+packages:
+  common: !include common.yaml
diff --git a/tests/components/sx126x/common.yaml b/tests/components/sx126x/common.yaml
new file mode 100644
index 0000000000..3f888c3ce4
--- /dev/null
+++ b/tests/components/sx126x/common.yaml
@@ -0,0 +1,40 @@
+spi:
+  clk_pin: ${clk_pin}
+  mosi_pin: ${mosi_pin}
+  miso_pin: ${miso_pin}
+
+sx126x:
+  dio1_pin: ${dio1_pin}
+  cs_pin: ${cs_pin}
+  busy_pin: ${busy_pin}
+  rst_pin: ${rst_pin}
+  pa_power: 3
+  bandwidth: 125_0kHz
+  crc_enable: true
+  frequency: 433920000
+  modulation: LORA
+  rx_start: true
+  hw_version: sx1262
+  rf_switch: true
+  sync_value: [0x14, 0x24]
+  preamble_size: 8
+  spreading_factor: 7
+  coding_rate: CR_4_6
+  tcxo_voltage: 1_8V
+  tcxo_delay: 5ms
+  on_packet:
+    then:
+      - lambda: |-
+          ESP_LOGD("lambda", "packet %.2f %.2f %s", rssi, snr, format_hex(x).c_str());
+
+button:
+  - platform: template
+    name: "SX126x Button"
+    on_press:
+      then:
+        - sx126x.set_mode_standby
+        - sx126x.run_image_cal
+        - sx126x.set_mode_sleep
+        - sx126x.set_mode_rx
+        - sx126x.send_packet:
+            data: [0xC5, 0x51, 0x78, 0x82, 0xB7, 0xF9, 0x9C, 0x5C]
diff --git a/tests/components/sx126x/test.esp32-ard.yaml b/tests/components/sx126x/test.esp32-ard.yaml
new file mode 100644
index 0000000000..9770f52229
--- /dev/null
+++ b/tests/components/sx126x/test.esp32-ard.yaml
@@ -0,0 +1,10 @@
+substitutions:
+  clk_pin: GPIO5
+  mosi_pin: GPIO27
+  miso_pin: GPIO19
+  cs_pin: GPIO18
+  rst_pin: GPIO23
+  busy_pin: GPIO25
+  dio1_pin: GPIO26
+
+<<: !include common.yaml
diff --git a/tests/components/sx126x/test.esp32-c3-ard.yaml b/tests/components/sx126x/test.esp32-c3-ard.yaml
new file mode 100644
index 0000000000..91450e24ce
--- /dev/null
+++ b/tests/components/sx126x/test.esp32-c3-ard.yaml
@@ -0,0 +1,10 @@
+substitutions:
+  clk_pin: GPIO5
+  mosi_pin: GPIO18
+  miso_pin: GPIO19
+  cs_pin: GPIO1
+  rst_pin: GPIO2
+  busy_pin: GPIO4
+  dio1_pin: GPIO3
+
+<<: !include common.yaml
diff --git a/tests/components/sx126x/test.esp32-c3-idf.yaml b/tests/components/sx126x/test.esp32-c3-idf.yaml
new file mode 100644
index 0000000000..91450e24ce
--- /dev/null
+++ b/tests/components/sx126x/test.esp32-c3-idf.yaml
@@ -0,0 +1,10 @@
+substitutions:
+  clk_pin: GPIO5
+  mosi_pin: GPIO18
+  miso_pin: GPIO19
+  cs_pin: GPIO1
+  rst_pin: GPIO2
+  busy_pin: GPIO4
+  dio1_pin: GPIO3
+
+<<: !include common.yaml
diff --git a/tests/components/sx126x/test.esp32-idf.yaml b/tests/components/sx126x/test.esp32-idf.yaml
new file mode 100644
index 0000000000..9770f52229
--- /dev/null
+++ b/tests/components/sx126x/test.esp32-idf.yaml
@@ -0,0 +1,10 @@
+substitutions:
+  clk_pin: GPIO5
+  mosi_pin: GPIO27
+  miso_pin: GPIO19
+  cs_pin: GPIO18
+  rst_pin: GPIO23
+  busy_pin: GPIO25
+  dio1_pin: GPIO26
+
+<<: !include common.yaml
diff --git a/tests/components/sx126x/test.esp8266-ard.yaml b/tests/components/sx126x/test.esp8266-ard.yaml
new file mode 100644
index 0000000000..d2c07c5bb7
--- /dev/null
+++ b/tests/components/sx126x/test.esp8266-ard.yaml
@@ -0,0 +1,10 @@
+substitutions:
+  clk_pin: GPIO5
+  mosi_pin: GPIO13
+  miso_pin: GPIO12
+  cs_pin: GPIO1
+  rst_pin: GPIO2
+  busy_pin: GPIO4
+  dio1_pin: GPIO3
+
+<<: !include common.yaml
diff --git a/tests/components/sx126x/test.rp2040-ard.yaml b/tests/components/sx126x/test.rp2040-ard.yaml
new file mode 100644
index 0000000000..8881e96971
--- /dev/null
+++ b/tests/components/sx126x/test.rp2040-ard.yaml
@@ -0,0 +1,10 @@
+substitutions:
+  clk_pin: GPIO2
+  mosi_pin: GPIO3
+  miso_pin: GPIO4
+  cs_pin: GPIO5
+  rst_pin: GPIO6
+  busy_pin: GPIO8
+  dio1_pin: GPIO7
+
+<<: !include common.yaml
diff --git a/tests/components/sx127x/common.yaml b/tests/components/sx127x/common.yaml
new file mode 100644
index 0000000000..63adc2e91c
--- /dev/null
+++ b/tests/components/sx127x/common.yaml
@@ -0,0 +1,45 @@
+spi:
+  clk_pin: ${clk_pin}
+  mosi_pin: ${mosi_pin}
+  miso_pin: ${miso_pin}
+
+sx127x:
+  cs_pin: ${cs_pin}
+  rst_pin: ${rst_pin}
+  dio0_pin: ${dio0_pin}
+  pa_pin: BOOST
+  pa_power: 17
+  pa_ramp: 40us
+  bitsync: true
+  bitrate: 4800
+  bandwidth: 50_0kHz
+  frequency: 433920000
+  modulation: FSK
+  deviation: 5000
+  rx_start: true
+  rx_floor: -90
+  packet_mode: true
+  payload_length: 8
+  sync_value: [0x33, 0x33]
+  shaping: NONE
+  preamble_size: 2
+  preamble_detect: 2
+  preamble_errors: 8
+  preamble_polarity: 0x55
+  on_packet:
+    then:
+      - sx127x.send_packet:
+          data: [0xC5, 0x51, 0x78, 0x82, 0xB7, 0xF9, 0x9C, 0x5C]
+
+button:
+  - platform: template
+    name: "SX127x Button"
+    on_press:
+      then:
+        - sx127x.set_mode_standby
+        - sx127x.run_image_cal
+        - sx127x.set_mode_tx
+        - sx127x.set_mode_sleep
+        - sx127x.set_mode_rx
+        - sx127x.send_packet:
+            data: [0xC5, 0x51, 0x78, 0x82, 0xB7, 0xF9, 0x9C, 0x5C]
diff --git a/tests/components/sx127x/test.esp32-ard.yaml b/tests/components/sx127x/test.esp32-ard.yaml
new file mode 100644
index 0000000000..71270462a2
--- /dev/null
+++ b/tests/components/sx127x/test.esp32-ard.yaml
@@ -0,0 +1,9 @@
+substitutions:
+  clk_pin: GPIO5
+  mosi_pin: GPIO27
+  miso_pin: GPIO19
+  cs_pin: GPIO18
+  rst_pin: GPIO23
+  dio0_pin: GPIO26
+
+<<: !include common.yaml
diff --git a/tests/components/sx127x/test.esp32-c3-ard.yaml b/tests/components/sx127x/test.esp32-c3-ard.yaml
new file mode 100644
index 0000000000..36535a950d
--- /dev/null
+++ b/tests/components/sx127x/test.esp32-c3-ard.yaml
@@ -0,0 +1,9 @@
+substitutions:
+  clk_pin: GPIO5
+  mosi_pin: GPIO18
+  miso_pin: GPIO19
+  cs_pin: GPIO1
+  rst_pin: GPIO2
+  dio0_pin: GPIO3
+
+<<: !include common.yaml
diff --git a/tests/components/sx127x/test.esp32-c3-idf.yaml b/tests/components/sx127x/test.esp32-c3-idf.yaml
new file mode 100644
index 0000000000..36535a950d
--- /dev/null
+++ b/tests/components/sx127x/test.esp32-c3-idf.yaml
@@ -0,0 +1,9 @@
+substitutions:
+  clk_pin: GPIO5
+  mosi_pin: GPIO18
+  miso_pin: GPIO19
+  cs_pin: GPIO1
+  rst_pin: GPIO2
+  dio0_pin: GPIO3
+
+<<: !include common.yaml
diff --git a/tests/components/sx127x/test.esp32-idf.yaml b/tests/components/sx127x/test.esp32-idf.yaml
new file mode 100644
index 0000000000..71270462a2
--- /dev/null
+++ b/tests/components/sx127x/test.esp32-idf.yaml
@@ -0,0 +1,9 @@
+substitutions:
+  clk_pin: GPIO5
+  mosi_pin: GPIO27
+  miso_pin: GPIO19
+  cs_pin: GPIO18
+  rst_pin: GPIO23
+  dio0_pin: GPIO26
+
+<<: !include common.yaml
diff --git a/tests/components/sx127x/test.esp8266-ard.yaml b/tests/components/sx127x/test.esp8266-ard.yaml
new file mode 100644
index 0000000000..64c01edd44
--- /dev/null
+++ b/tests/components/sx127x/test.esp8266-ard.yaml
@@ -0,0 +1,9 @@
+substitutions:
+  clk_pin: GPIO5
+  mosi_pin: GPIO13
+  miso_pin: GPIO12
+  cs_pin: GPIO1
+  rst_pin: GPIO2
+  dio0_pin: GPIO3
+
+<<: !include common.yaml
diff --git a/tests/components/sx127x/test.rp2040-ard.yaml b/tests/components/sx127x/test.rp2040-ard.yaml
new file mode 100644
index 0000000000..0af7b29790
--- /dev/null
+++ b/tests/components/sx127x/test.rp2040-ard.yaml
@@ -0,0 +1,9 @@
+substitutions:
+  clk_pin: GPIO2
+  mosi_pin: GPIO3
+  miso_pin: GPIO4
+  cs_pin: GPIO5
+  rst_pin: GPIO6
+  dio0_pin: GPIO7
+
+<<: !include common.yaml
diff --git a/tests/components/syslog/test.ln882x-ard.yaml b/tests/components/syslog/test.ln882x-ard.yaml
new file mode 100644
index 0000000000..dade44d145
--- /dev/null
+++ b/tests/components/syslog/test.ln882x-ard.yaml
@@ -0,0 +1 @@
+<<: !include common.yaml
diff --git a/tests/components/template/test.ln882x-ard.yaml b/tests/components/template/test.ln882x-ard.yaml
new file mode 100644
index 0000000000..25cb37a0b4
--- /dev/null
+++ b/tests/components/template/test.ln882x-ard.yaml
@@ -0,0 +1,2 @@
+packages:
+  common: !include common.yaml
diff --git a/tests/components/update/common.yaml b/tests/components/update/common.yaml
index dcb4f42527..45ed110352 100644
--- a/tests/components/update/common.yaml
+++ b/tests/components/update/common.yaml
@@ -26,3 +26,5 @@ update:
   - platform: http_request
     name: Firmware Update
     source: http://example.com/manifest.json
+    on_update_available:
+      - logger.log: "A new update is available"
diff --git a/tests/components/web_server/test_no_ota.esp32-idf.yaml b/tests/components/web_server/test_no_ota.esp32-idf.yaml
new file mode 100644
index 0000000000..4064f518cf
--- /dev/null
+++ b/tests/components/web_server/test_no_ota.esp32-idf.yaml
@@ -0,0 +1,16 @@
+esphome:
+  name: test-web-server-no-ota-idf
+
+esp32:
+  board: esp32dev
+  framework:
+    type: esp-idf
+
+packages:
+  device_base: !include common.yaml
+
+# No OTA component defined for this test
+
+web_server:
+  port: 8080
+  version: 2
diff --git a/tests/components/web_server/test_ota.esp32-idf.yaml b/tests/components/web_server/test_ota.esp32-idf.yaml
new file mode 100644
index 0000000000..37838b3d34
--- /dev/null
+++ b/tests/components/web_server/test_ota.esp32-idf.yaml
@@ -0,0 +1,30 @@
+esphome:
+  name: test-web-server-ota-idf
+
+esp32:
+  board: esp32dev
+  framework:
+    type: esp-idf
+
+packages:
+  device_base: !include common.yaml
+
+# Enable OTA for multipart upload testing
+ota:
+  - platform: esphome
+    password: "test_ota_password"
+  - platform: web_server
+
+# Web server configuration
+web_server:
+  port: 8080
+  version: 2
+  include_internal: true
+
+# Enable debug logging for OTA
+logger:
+  level: VERBOSE
+  logs:
+    web_server: VERBOSE
+    web_server_idf: VERBOSE
+
diff --git a/tests/components/web_server/test_ota_disabled.esp32-idf.yaml b/tests/components/web_server/test_ota_disabled.esp32-idf.yaml
new file mode 100644
index 0000000000..b88b845db7
--- /dev/null
+++ b/tests/components/web_server/test_ota_disabled.esp32-idf.yaml
@@ -0,0 +1,18 @@
+esphome:
+  name: test-ws-ota-disabled-idf
+
+esp32:
+  board: esp32dev
+  framework:
+    type: esp-idf
+
+packages:
+  device_base: !include common.yaml
+
+# OTA is configured but web_server OTA is NOT included
+ota:
+  - platform: esphome
+
+web_server:
+  port: 8080
+  version: 2
diff --git a/tests/components/wled/test.esp32-ard.yaml b/tests/components/wled/test.esp32-ard.yaml
index a24f28e154..156b31181e 100644
--- a/tests/components/wled/test.esp32-ard.yaml
+++ b/tests/components/wled/test.esp32-ard.yaml
@@ -12,6 +12,5 @@ light:
     rgb_order: GRB
     num_leds: 256
     pin: 2
-    rmt_channel: 0
     effects:
       - wled:
diff --git a/tests/components/wled/test.esp32-c3-ard.yaml b/tests/components/wled/test.esp32-c3-ard.yaml
index a24f28e154..156b31181e 100644
--- a/tests/components/wled/test.esp32-c3-ard.yaml
+++ b/tests/components/wled/test.esp32-c3-ard.yaml
@@ -12,6 +12,5 @@ light:
     rgb_order: GRB
     num_leds: 256
     pin: 2
-    rmt_channel: 0
     effects:
       - wled:
diff --git a/tests/components/xiaomi_xmwsdj04mmc/common.yaml b/tests/components/xiaomi_xmwsdj04mmc/common.yaml
new file mode 100644
index 0000000000..fe7a11efc5
--- /dev/null
+++ b/tests/components/xiaomi_xmwsdj04mmc/common.yaml
@@ -0,0 +1,12 @@
+esp32_ble_tracker:
+
+sensor:
+  - platform: xiaomi_xmwsdj04mmc
+    mac_address: 84:B4:DB:5D:A3:8F
+    bindkey: d8ca2ed09bb5541dc8f045ca360b00ea
+    temperature:
+      name: Xiaomi XMWSDJ04MMC Temperature
+    humidity:
+      name: Xiaomi XMWSDJ04MMC Humidity
+    battery_level:
+      name: Xiaomi XMWSDJ04MMC Battery Level
diff --git a/tests/components/xiaomi_xmwsdj04mmc/test.esp32-ard.yaml b/tests/components/xiaomi_xmwsdj04mmc/test.esp32-ard.yaml
new file mode 100644
index 0000000000..dade44d145
--- /dev/null
+++ b/tests/components/xiaomi_xmwsdj04mmc/test.esp32-ard.yaml
@@ -0,0 +1 @@
+<<: !include common.yaml
diff --git a/tests/components/xiaomi_xmwsdj04mmc/test.esp32-c3-ard.yaml b/tests/components/xiaomi_xmwsdj04mmc/test.esp32-c3-ard.yaml
new file mode 100644
index 0000000000..dade44d145
--- /dev/null
+++ b/tests/components/xiaomi_xmwsdj04mmc/test.esp32-c3-ard.yaml
@@ -0,0 +1 @@
+<<: !include common.yaml
diff --git a/tests/components/xiaomi_xmwsdj04mmc/test.esp32-c3-idf.yaml b/tests/components/xiaomi_xmwsdj04mmc/test.esp32-c3-idf.yaml
new file mode 100644
index 0000000000..dade44d145
--- /dev/null
+++ b/tests/components/xiaomi_xmwsdj04mmc/test.esp32-c3-idf.yaml
@@ -0,0 +1 @@
+<<: !include common.yaml
diff --git a/tests/components/xiaomi_xmwsdj04mmc/test.esp32-idf.yaml b/tests/components/xiaomi_xmwsdj04mmc/test.esp32-idf.yaml
new file mode 100644
index 0000000000..dade44d145
--- /dev/null
+++ b/tests/components/xiaomi_xmwsdj04mmc/test.esp32-idf.yaml
@@ -0,0 +1 @@
+<<: !include common.yaml
diff --git a/tests/dummy_main.cpp b/tests/dummy_main.cpp
index 3ba4c8bd07..afd393c095 100644
--- a/tests/dummy_main.cpp
+++ b/tests/dummy_main.cpp
@@ -12,7 +12,7 @@
 using namespace esphome;
 
 void setup() {
-  App.pre_setup("livingroom", "LivingRoom", "LivingRoomArea", "comment", __DATE__ ", " __TIME__, false);
+  App.pre_setup("livingroom", "LivingRoom", "comment", __DATE__ ", " __TIME__, false);
   auto *log = new logger::Logger(115200, 512);  // NOLINT
   log->pre_setup();
   log->set_uart_selection(logger::UART_SELECTION_UART0);
diff --git a/tests/integration/README.md b/tests/integration/README.md
index 26bd5a00ee..8fce81bb80 100644
--- a/tests/integration/README.md
+++ b/tests/integration/README.md
@@ -78,3 +78,268 @@ pytest -s tests/integration/test_host_mode_basic.py
 - Each test gets its own temporary directory and unique port
 - Port allocation minimizes race conditions by holding the socket until just before ESPHome starts
 - Output from ESPHome processes is displayed for debugging
+
+## Integration Test Writing Guide
+
+### Test Patterns and Best Practices
+
+#### 1. Test File Naming Convention
+- Use descriptive names: `test_{category}_{feature}.py`
+- Common categories: `host_mode`, `api`, `scheduler`, `light`, `areas_and_devices`
+- Examples:
+  - `test_host_mode_basic.py` - Basic host mode functionality
+  - `test_api_message_batching.py` - API message batching
+  - `test_scheduler_stress.py` - Scheduler stress testing
+
+#### 2. Essential Imports
+```python
+from __future__ import annotations
+
+import asyncio
+from typing import Any
+
+import pytest
+from aioesphomeapi import EntityState, SensorState
+
+from .types import APIClientConnectedFactory, RunCompiledFunction
+```
+
+#### 3. Common Test Patterns
+
+##### Basic Entity Test
+```python
+@pytest.mark.asyncio
+async def test_my_sensor(
+    yaml_config: str,
+    run_compiled: RunCompiledFunction,
+    api_client_connected: APIClientConnectedFactory,
+) -> None:
+    """Test sensor functionality."""
+    async with run_compiled(yaml_config), api_client_connected() as client:
+        # Get entity list
+        entities, services = await client.list_entities_services()
+
+        # Find specific entity
+        sensor = next((e for e in entities if e.object_id == "my_sensor"), None)
+        assert sensor is not None
+```
+
+##### State Subscription Pattern
+```python
+# Track state changes with futures
+loop = asyncio.get_running_loop()
+states: dict[int, EntityState] = {}
+state_future: asyncio.Future[EntityState] = loop.create_future()
+
+def on_state(state: EntityState) -> None:
+    states[state.key] = state
+    # Check for specific condition using isinstance
+    if isinstance(state, SensorState) and state.state == expected_value:
+        if not state_future.done():
+            state_future.set_result(state)
+
+client.subscribe_states(on_state)
+
+# Wait for state with timeout
+try:
+    result = await asyncio.wait_for(state_future, timeout=5.0)
+except asyncio.TimeoutError:
+    pytest.fail(f"Expected state not received. Got: {list(states.values())}")
+```
+
+##### Service Execution Pattern
+```python
+# Find and execute service
+entities, services = await client.list_entities_services()
+my_service = next((s for s in services if s.name == "my_service"), None)
+assert my_service is not None
+
+# Execute with parameters
+client.execute_service(my_service, {"param1": "value1", "param2": 42})
+```
+
+##### Multiple Entity Tracking
+```python
+# For tests with many entities
+loop = asyncio.get_running_loop()
+entity_count = 50
+received_states: set[int] = set()
+all_states_future: asyncio.Future[bool] = loop.create_future()
+
+def on_state(state: EntityState) -> None:
+    received_states.add(state.key)
+    if len(received_states) >= entity_count and not all_states_future.done():
+        all_states_future.set_result(True)
+
+client.subscribe_states(on_state)
+await asyncio.wait_for(all_states_future, timeout=10.0)
+```
+
+#### 4. YAML Fixture Guidelines
+
+##### Naming Convention
+- Match test function name: `test_my_feature` → `fixtures/my_feature.yaml`
+- Note: Remove `test_` prefix for fixture filename
+
+##### Basic Structure
+```yaml
+esphome:
+  name: test-name  # Use kebab-case
+  # Optional: areas, devices, platformio_options
+
+host:  # Always use host platform for integration tests
+api:   # Port injected automatically
+logger:
+  level: DEBUG  # Optional: Set log level
+
+# Component configurations
+sensor:
+  - platform: template
+    name: "My Sensor"
+    id: my_sensor
+    lambda: return 42.0;
+    update_interval: 0.1s  # Fast updates for testing
+```
+
+##### Advanced Features
+```yaml
+# External components for custom test code
+external_components:
+  - source:
+      type: local
+      path: EXTERNAL_COMPONENT_PATH  # Replaced by test framework
+    components: [my_test_component]
+
+# Areas and devices
+esphome:
+  name: test-device
+  areas:
+    - id: living_room
+      name: "Living Room"
+    - id: kitchen
+      name: "Kitchen"
+      parent_id: living_room
+  devices:
+    - id: my_device
+      name: "Test Device"
+      area_id: living_room
+
+# API services
+api:
+  services:
+    - service: test_service
+      variables:
+        my_param: string
+      then:
+        - logger.log:
+            format: "Service called with: %s"
+            args: [my_param.c_str()]
+```
+
+#### 5. Testing Complex Scenarios
+
+##### External Components
+Create C++ components in `fixtures/external_components/` for:
+- Stress testing
+- Custom entity behaviors
+- Scheduler testing
+- Memory management tests
+
+##### Log Line Monitoring
+```python
+log_lines: list[str] = []
+
+def on_log_line(line: str) -> None:
+    log_lines.append(line)
+    if "expected message" in line:
+        # Handle specific log messages
+
+async with run_compiled(yaml_config, line_callback=on_log_line):
+    # Test implementation
+```
+
+Example using futures for specific log patterns:
+```python
+import re
+
+loop = asyncio.get_running_loop()
+connected_future = loop.create_future()
+service_future = loop.create_future()
+
+# Patterns to match
+connected_pattern = re.compile(r"Client .* connected from")
+service_pattern = re.compile(r"Service called")
+
+def check_output(line: str) -> None:
+    """Check log output for expected messages."""
+    if not connected_future.done() and connected_pattern.search(line):
+        connected_future.set_result(True)
+    elif not service_future.done() and service_pattern.search(line):
+        service_future.set_result(True)
+
+async with run_compiled(yaml_config, line_callback=check_output):
+    async with api_client_connected() as client:
+        # Wait for specific log message
+        await asyncio.wait_for(connected_future, timeout=5.0)
+
+        # Do test actions...
+
+        # Wait for service log
+        await asyncio.wait_for(service_future, timeout=5.0)
+```
+
+**Note**: Tests that monitor log messages typically have fewer race conditions compared to state-based testing, making them more reliable. However, be aware that the host platform currently does not have a thread-safe logger, so logging from threads will not work correctly.
+
+##### Timeout Handling
+```python
+# Always use timeouts for async operations
+try:
+    result = await asyncio.wait_for(some_future, timeout=5.0)
+except asyncio.TimeoutError:
+    pytest.fail("Operation timed out - check test expectations")
+```
+
+#### 6. Common Assertions
+
+```python
+# Device info
+assert device_info.name == "expected-name"
+assert device_info.compilation_time is not None
+
+# Entity properties
+assert sensor.accuracy_decimals == 2
+assert sensor.state_class == 1  # measurement
+assert sensor.force_update is True
+
+# Service availability
+assert len(services) > 0
+assert any(s.name == "expected_service" for s in services)
+
+# State values
+assert state.state == expected_value
+assert state.missing_state is False
+```
+
+#### 7. Debugging Tips
+
+- Use `pytest -s` to see ESPHome output during tests
+- Add descriptive failure messages to assertions
+- Use `pytest.fail()` with detailed error info for timeouts
+- Check `log_lines` for compilation or runtime errors
+- Enable debug logging in YAML fixtures when needed
+
+#### 8. Performance Considerations
+
+- Use short update intervals (0.1s) for faster tests
+- Set reasonable timeouts (5-10s for most operations)
+- Batch multiple assertions when possible
+- Clean up resources properly using context managers
+
+#### 9. Test Categories
+
+- **Basic Tests**: Minimal functionality verification
+- **Entity Tests**: Sensor, switch, light behavior
+- **API Tests**: Message batching, services, events
+- **Scheduler Tests**: Timing, defer operations, stress
+- **Memory Tests**: Conditional compilation, optimization
+- **Integration Tests**: Areas, devices, complex interactions
diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py
index 90377300a6..aead6a73af 100644
--- a/tests/integration/conftest.py
+++ b/tests/integration/conftest.py
@@ -3,7 +3,7 @@
 from __future__ import annotations
 
 import asyncio
-from collections.abc import AsyncGenerator, Generator
+from collections.abc import AsyncGenerator, Callable, Generator
 from contextlib import AbstractAsyncContextManager, asynccontextmanager
 import logging
 import os
@@ -46,6 +46,7 @@ if platform.system() == "Windows":
         "Integration tests are not supported on Windows", allow_module_level=True
     )
 
+
 import pty  # not available on Windows
 
 
@@ -164,6 +165,19 @@ async def compile_esphome(
     """Compile an ESPHome configuration and return the binary path."""
 
     async def _compile(config_path: Path) -> Path:
+        # Create a unique PlatformIO directory for this test to avoid race conditions
+        platformio_dir = integration_test_dir / ".platformio"
+        platformio_dir.mkdir(parents=True, exist_ok=True)
+
+        # Create cache directory as well
+        platformio_cache_dir = platformio_dir / ".cache"
+        platformio_cache_dir.mkdir(parents=True, exist_ok=True)
+
+        # Set up environment with isolated PlatformIO directories
+        env = os.environ.copy()
+        env["PLATFORMIO_CORE_DIR"] = str(platformio_dir)
+        env["PLATFORMIO_CACHE_DIR"] = str(platformio_cache_dir)
+
         # Retry compilation up to 3 times if we get a segfault
         max_retries = 3
         for attempt in range(max_retries):
@@ -178,6 +192,7 @@ async def compile_esphome(
                 stdin=asyncio.subprocess.DEVNULL,
                 # Start in a new process group to isolate signal handling
                 start_new_session=True,
+                env=env,
             )
             await proc.wait()
 
@@ -202,6 +217,7 @@ async def compile_esphome(
         loop = asyncio.get_running_loop()
 
         def _read_config_and_get_binary():
+            CORE.reset()  # Reset CORE state between test runs
             CORE.config_path = str(config_path)
             config = esphome.config.read_config(
                 {"command": "compile", "config": str(config_path)}
@@ -362,7 +378,10 @@ async def api_client_connected(
 
 
 async def _read_stream_lines(
-    stream: asyncio.StreamReader, lines: list[str], output_stream: TextIO
+    stream: asyncio.StreamReader,
+    lines: list[str],
+    output_stream: TextIO,
+    line_callback: Callable[[str], None] | None = None,
 ) -> None:
     """Read lines from a stream, append to list, and echo to output stream."""
     log_parser = LogParser()
@@ -380,6 +399,9 @@ async def _read_stream_lines(
             file=output_stream,
             flush=True,
         )
+        # Call the callback if provided
+        if line_callback:
+            line_callback(decoded_line.rstrip())
 
 
 @asynccontextmanager
@@ -388,6 +410,7 @@ async def run_binary_and_wait_for_port(
     host: str,
     port: int,
     timeout: float = PORT_WAIT_TIMEOUT,
+    line_callback: Callable[[str], None] | None = None,
 ) -> AsyncGenerator[None]:
     """Run a binary, wait for it to open a port, and clean up on exit."""
     # Create a pseudo-terminal to make the binary think it's running interactively
@@ -435,7 +458,9 @@ async def run_binary_and_wait_for_port(
         # Read from output stream
         output_tasks = [
             asyncio.create_task(
-                _read_stream_lines(output_reader, stdout_lines, sys.stdout)
+                _read_stream_lines(
+                    output_reader, stdout_lines, sys.stdout, line_callback
+                )
             )
         ]
 
@@ -515,6 +540,7 @@ async def run_compiled_context(
     compile_esphome: CompileFunction,
     port: int,
     port_socket: socket.socket | None = None,
+    line_callback: Callable[[str], None] | None = None,
 ) -> AsyncGenerator[None]:
     """Context manager to write, compile and run an ESPHome configuration."""
     # Write the YAML config
@@ -528,7 +554,9 @@ async def run_compiled_context(
         port_socket.close()
 
     # Run the binary and wait for the API server to start
-    async with run_binary_and_wait_for_port(binary_path, LOCALHOST, port):
+    async with run_binary_and_wait_for_port(
+        binary_path, LOCALHOST, port, line_callback=line_callback
+    ):
         yield
 
 
@@ -542,7 +570,9 @@ async def run_compiled(
     port, port_socket = reserved_tcp_port
 
     def _run_compiled(
-        yaml_content: str, filename: str | None = None
+        yaml_content: str,
+        filename: str | None = None,
+        line_callback: Callable[[str], None] | None = None,
     ) -> AbstractAsyncContextManager[asyncio.subprocess.Process]:
         return run_compiled_context(
             yaml_content,
@@ -551,6 +581,7 @@ async def run_compiled(
             compile_esphome,
             port,
             port_socket,
+            line_callback=line_callback,
         )
 
     yield _run_compiled
diff --git a/tests/integration/fixtures/api_conditional_memory.yaml b/tests/integration/fixtures/api_conditional_memory.yaml
new file mode 100644
index 0000000000..22e8ed79d6
--- /dev/null
+++ b/tests/integration/fixtures/api_conditional_memory.yaml
@@ -0,0 +1,29 @@
+esphome:
+  name: api-conditional-memory-test
+host:
+api:
+  actions:
+    - action: test_simple_service
+      then:
+        - logger.log: "Simple service called"
+    - action: test_service_with_args
+      variables:
+        arg_string: string
+        arg_int: int
+        arg_bool: bool
+        arg_float: float
+      then:
+        - logger.log:
+            format: "Service called with: %s, %d, %d, %.2f"
+            args: [arg_string.c_str(), arg_int, arg_bool, arg_float]
+  on_client_connected:
+    - logger.log:
+        format: "Client %s connected from %s"
+        args: [client_info.c_str(), client_address.c_str()]
+  on_client_disconnected:
+    - logger.log:
+        format: "Client %s disconnected from %s"
+        args: [client_info.c_str(), client_address.c_str()]
+
+logger:
+  level: DEBUG
diff --git a/tests/integration/fixtures/api_reboot_timeout.yaml b/tests/integration/fixtures/api_reboot_timeout.yaml
new file mode 100644
index 0000000000..881bb5b2fc
--- /dev/null
+++ b/tests/integration/fixtures/api_reboot_timeout.yaml
@@ -0,0 +1,7 @@
+esphome:
+  name: api-reboot-test
+host:
+api:
+  reboot_timeout: 0.5s  # Very short timeout for fast testing
+logger:
+  level: DEBUG
diff --git a/tests/integration/fixtures/api_vv_logging.yaml b/tests/integration/fixtures/api_vv_logging.yaml
new file mode 100644
index 0000000000..df1edc796a
--- /dev/null
+++ b/tests/integration/fixtures/api_vv_logging.yaml
@@ -0,0 +1,89 @@
+esphome:
+  name: vv-logging-test
+
+host:
+
+api:
+
+logger:
+  level: VERY_VERBOSE
+  # Enable VV logging for API components where the issue occurs
+  logs:
+    api.connection: VERY_VERBOSE
+    api.service: VERY_VERBOSE
+    api.proto: VERY_VERBOSE
+    sensor: VERY_VERBOSE
+
+# Create many sensors that update frequently to generate API traffic
+# This will cause many messages to be batched and sent, triggering the
+# code path where VV logging could cause buffer corruption
+sensor:
+  - platform: template
+    name: "Test Sensor 1"
+    lambda: 'return millis() / 1000.0;'
+    update_interval: 50ms
+    unit_of_measurement: "s"
+
+  - platform: template
+    name: "Test Sensor 2"
+    lambda: 'return (millis() / 1000.0) + 10;'
+    update_interval: 50ms
+    unit_of_measurement: "s"
+
+  - platform: template
+    name: "Test Sensor 3"
+    lambda: 'return (millis() / 1000.0) + 20;'
+    update_interval: 50ms
+    unit_of_measurement: "s"
+
+  - platform: template
+    name: "Test Sensor 4"
+    lambda: 'return (millis() / 1000.0) + 30;'
+    update_interval: 50ms
+    unit_of_measurement: "s"
+
+  - platform: template
+    name: "Test Sensor 5"
+    lambda: 'return (millis() / 1000.0) + 40;'
+    update_interval: 50ms
+    unit_of_measurement: "s"
+
+  - platform: template
+    name: "Test Sensor 6"
+    lambda: 'return (millis() / 1000.0) + 50;'
+    update_interval: 50ms
+    unit_of_measurement: "s"
+
+  - platform: template
+    name: "Test Sensor 7"
+    lambda: 'return (millis() / 1000.0) + 60;'
+    update_interval: 50ms
+    unit_of_measurement: "s"
+
+  - platform: template
+    name: "Test Sensor 8"
+    lambda: 'return (millis() / 1000.0) + 70;'
+    update_interval: 50ms
+    unit_of_measurement: "s"
+
+  - platform: template
+    name: "Test Sensor 9"
+    lambda: 'return (millis() / 1000.0) + 80;'
+    update_interval: 50ms
+    unit_of_measurement: "s"
+
+  - platform: template
+    name: "Test Sensor 10"
+    lambda: 'return (millis() / 1000.0) + 90;'
+    update_interval: 50ms
+    unit_of_measurement: "s"
+
+# Add some binary sensors too for variety
+binary_sensor:
+  - platform: template
+    name: "Test Binary 1"
+    lambda: 'return (millis() / 1000) % 2 == 0;'
+
+  - platform: template
+    name: "Test Binary 2"
+    lambda: 'return (millis() / 1000) % 3 == 0;'
diff --git a/tests/integration/fixtures/areas_and_devices.yaml b/tests/integration/fixtures/areas_and_devices.yaml
new file mode 100644
index 0000000000..4a327b73a1
--- /dev/null
+++ b/tests/integration/fixtures/areas_and_devices.yaml
@@ -0,0 +1,57 @@
+esphome:
+  name: areas-devices-test
+  # Define top-level area
+  area:
+    id: living_room_area
+    name: Living Room
+  # Define additional areas
+  areas:
+    - id: bedroom_area
+      name: Bedroom
+    - id: kitchen_area
+      name: Kitchen
+  # Define devices with area assignments
+  devices:
+    - id: light_controller_device
+      name: Light Controller
+      area_id: living_room_area  # Uses top-level area
+    - id: temp_sensor_device
+      name: Temperature Sensor
+      area_id: bedroom_area
+    - id: motion_detector_device
+      name: Motion Detector
+      area_id: living_room_area  # Reuses top-level area
+    - id: smart_switch_device
+      name: Smart Switch
+      area_id: kitchen_area
+
+host:
+api:
+logger:
+
+# Sensors assigned to different devices
+sensor:
+  - platform: template
+    name: Light Controller Sensor
+    device_id: light_controller_device
+    lambda: return 1.0;
+    update_interval: 0.1s
+
+  - platform: template
+    name: Temperature Sensor Reading
+    device_id: temp_sensor_device
+    lambda: return 2.0;
+    update_interval: 0.1s
+
+  - platform: template
+    name: Motion Detector Status
+    device_id: motion_detector_device
+    lambda: return 3.0;
+    update_interval: 0.1s
+
+  - platform: template
+    name: Smart Switch Power
+    device_id: smart_switch_device
+    lambda: return 4.0;
+    update_interval: 0.1s
+
diff --git a/tests/integration/fixtures/batch_delay_zero_rapid_transitions.yaml b/tests/integration/fixtures/batch_delay_zero_rapid_transitions.yaml
new file mode 100644
index 0000000000..32cacfaa79
--- /dev/null
+++ b/tests/integration/fixtures/batch_delay_zero_rapid_transitions.yaml
@@ -0,0 +1,43 @@
+esphome:
+  name: rapid-transitions-test
+host:
+api:
+  batch_delay: 0ms  # Enable immediate sending for rapid transitions
+logger:
+  level: DEBUG
+
+# Add a sensor that updates frequently to trigger lambda evaluations
+sensor:
+  - platform: template
+    name: "Update Trigger"
+    id: update_trigger
+    lambda: |-
+      return 0;
+    update_interval: 10ms
+    internal: true
+
+# Simulate an IR remote binary sensor with rapid ON/OFF transitions
+binary_sensor:
+  - platform: template
+    name: "Simulated IR Remote Button"
+    id: ir_remote_button
+    lambda: |-
+      // Simulate rapid button presses every ~100ms
+      // Each "press" is ON for ~30ms then OFF
+      uint32_t now = millis();
+      uint32_t press_cycle = now % 100;  // 100ms cycle
+
+      // ON for first 30ms of each cycle
+      if (press_cycle < 30) {
+        // Only log state change
+        if (!id(ir_remote_button).state) {
+          ESP_LOGD("test", "Button ON at %u", now);
+        }
+        return true;
+      } else {
+        // Only log state change
+        if (id(ir_remote_button).state) {
+          ESP_LOGD("test", "Button OFF at %u", now);
+        }
+        return false;
+      }
diff --git a/tests/integration/fixtures/device_id_in_state.yaml b/tests/integration/fixtures/device_id_in_state.yaml
new file mode 100644
index 0000000000..f2e320a2e2
--- /dev/null
+++ b/tests/integration/fixtures/device_id_in_state.yaml
@@ -0,0 +1,85 @@
+esphome:
+  name: device-id-state-test
+  # Define areas
+  areas:
+    - id: living_room
+      name: Living Room
+    - id: bedroom
+      name: Bedroom
+  # Define devices
+  devices:
+    - id: temperature_monitor
+      name: Temperature Monitor
+      area_id: living_room
+    - id: humidity_monitor
+      name: Humidity Monitor
+      area_id: bedroom
+    - id: motion_sensor
+      name: Motion Sensor
+      area_id: living_room
+
+host:
+api:
+logger:
+
+# Test different entity types with device assignments
+sensor:
+  - platform: template
+    name: Temperature
+    device_id: temperature_monitor
+    lambda: return 25.5;
+    update_interval: 0.1s
+    unit_of_measurement: "°C"
+
+  - platform: template
+    name: Humidity
+    device_id: humidity_monitor
+    lambda: return 65.0;
+    update_interval: 0.1s
+    unit_of_measurement: "%"
+
+  # Test entity without device_id (should have device_id 0)
+  - platform: template
+    name: No Device Sensor
+    lambda: return 100.0;
+    update_interval: 0.1s
+
+binary_sensor:
+  - platform: template
+    name: Motion Detected
+    device_id: motion_sensor
+    lambda: return true;
+
+switch:
+  - platform: template
+    name: Temperature Monitor Power
+    device_id: temperature_monitor
+    lambda: return true;
+    turn_on_action:
+      - lambda: |-
+          ESP_LOGD("test", "Turning on");
+    turn_off_action:
+      - lambda: |-
+          ESP_LOGD("test", "Turning off");
+
+text_sensor:
+  - platform: template
+    name: Temperature Status
+    device_id: temperature_monitor
+    lambda: return {"Normal"};
+    update_interval: 0.1s
+
+light:
+  - platform: binary
+    name: Motion Light
+    device_id: motion_sensor
+    output: motion_light_output
+
+output:
+  - platform: template
+    id: motion_light_output
+    type: binary
+    write_action:
+      - lambda: |-
+          ESP_LOGD("test", "Light output: %d", state);
+
diff --git a/tests/integration/fixtures/duplicate_entities_not_allowed_on_different_devices.yaml b/tests/integration/fixtures/duplicate_entities_not_allowed_on_different_devices.yaml
new file mode 100644
index 0000000000..f7d017a0ae
--- /dev/null
+++ b/tests/integration/fixtures/duplicate_entities_not_allowed_on_different_devices.yaml
@@ -0,0 +1,182 @@
+esphome:
+  name: duplicate-entities-test
+  # Define devices to test multi-device unique name validation
+  devices:
+    - id: controller_1
+      name: Controller 1
+    - id: controller_2
+      name: Controller 2
+    - id: controller_3
+      name: Controller 3
+
+host:
+api:  # Port will be automatically injected
+logger:
+
+# Test that duplicate entity names are NOT allowed on different devices
+
+# Scenario 1: Different sensor names on different devices (allowed)
+sensor:
+  - platform: template
+    name: Temperature Controller 1
+    device_id: controller_1
+    lambda: return 21.0;
+    update_interval: 0.1s
+
+  - platform: template
+    name: Temperature Controller 2
+    device_id: controller_2
+    lambda: return 22.0;
+    update_interval: 0.1s
+
+  - platform: template
+    name: Temperature Controller 3
+    device_id: controller_3
+    lambda: return 23.0;
+    update_interval: 0.1s
+
+  # Main device sensor (no device_id)
+  - platform: template
+    name: Temperature Main
+    lambda: return 20.0;
+    update_interval: 0.1s
+
+  # Different sensor with unique name
+  - platform: template
+    name: Humidity
+    lambda: return 60.0;
+    update_interval: 0.1s
+
+# Scenario 2: Different binary sensor names on different devices
+binary_sensor:
+  - platform: template
+    name: Status Controller 1
+    device_id: controller_1
+    lambda: return true;
+
+  - platform: template
+    name: Status Controller 2
+    device_id: controller_2
+    lambda: return false;
+
+  - platform: template
+    name: Status Main
+    lambda: return true;  # Main device
+
+  # Different platform can have same name as sensor
+  - platform: template
+    name: Temperature
+    lambda: return true;
+
+# Scenario 3: Different text sensor names on different devices
+text_sensor:
+  - platform: template
+    name: Device Info Controller 1
+    device_id: controller_1
+    lambda: return {"Controller 1 Active"};
+    update_interval: 0.1s
+
+  - platform: template
+    name: Device Info Controller 2
+    device_id: controller_2
+    lambda: return {"Controller 2 Active"};
+    update_interval: 0.1s
+
+  - platform: template
+    name: Device Info Main
+    lambda: return {"Main Device Active"};
+    update_interval: 0.1s
+
+# Scenario 4: Different switch names on different devices
+switch:
+  - platform: template
+    name: Power Controller 1
+    device_id: controller_1
+    lambda: return false;
+    turn_on_action: []
+    turn_off_action: []
+
+  - platform: template
+    name: Power Controller 2
+    device_id: controller_2
+    lambda: return true;
+    turn_on_action: []
+    turn_off_action: []
+
+  - platform: template
+    name: Power Controller 3
+    device_id: controller_3
+    lambda: return false;
+    turn_on_action: []
+    turn_off_action: []
+
+  # Unique switch on main device
+  - platform: template
+    name: Main Power
+    lambda: return true;
+    turn_on_action: []
+    turn_off_action: []
+
+# Scenario 5: Buttons with unique names
+button:
+  - platform: template
+    name: "Reset Controller 1"
+    device_id: controller_1
+    on_press: []
+
+  - platform: template
+    name: "Reset Controller 2"
+    device_id: controller_2
+    on_press: []
+
+  - platform: template
+    name: "Reset Main"
+    on_press: []  # Main device
+
+# Scenario 6: Empty names (should use device names)
+select:
+  - platform: template
+    name: ""
+    device_id: controller_1
+    options:
+      - "Option 1"
+      - "Option 2"
+    lambda: return {"Option 1"};
+    set_action: []
+
+  - platform: template
+    name: ""
+    device_id: controller_2
+    options:
+      - "Option 1"
+      - "Option 2"
+    lambda: return {"Option 1"};
+    set_action: []
+
+  - platform: template
+    name: ""  # Main device
+    options:
+      - "Option 1"
+      - "Option 2"
+    lambda: return {"Option 1"};
+    set_action: []
+
+# Scenario 7: Special characters in names - now with unique names
+number:
+  - platform: template
+    name: "Temperature Setpoint! Controller 1"
+    device_id: controller_1
+    min_value: 10.0
+    max_value: 30.0
+    step: 0.1
+    lambda: return 21.0;
+    set_action: []
+
+  - platform: template
+    name: "Temperature Setpoint! Controller 2"
+    device_id: controller_2
+    min_value: 10.0
+    max_value: 30.0
+    step: 0.1
+    lambda: return 22.0;
+    set_action: []
diff --git a/tests/integration/fixtures/entity_icon.yaml b/tests/integration/fixtures/entity_icon.yaml
new file mode 100644
index 0000000000..2ce633fe2c
--- /dev/null
+++ b/tests/integration/fixtures/entity_icon.yaml
@@ -0,0 +1,78 @@
+esphome:
+  name: icon-test
+
+host:
+
+api:
+
+logger:
+
+# Test entities with custom icons
+sensor:
+  - platform: template
+    name: "Sensor With Icon"
+    icon: "mdi:temperature-celsius"
+    unit_of_measurement: "°C"
+    update_interval: 1s
+    lambda: |-
+      return 25.5;
+
+  - platform: template
+    name: "Sensor Without Icon"
+    unit_of_measurement: "%"
+    update_interval: 1s
+    lambda: |-
+      return 50.0;
+
+binary_sensor:
+  - platform: template
+    name: "Binary Sensor With Icon"
+    icon: "mdi:motion-sensor"
+    lambda: |-
+      return true;
+
+  - platform: template
+    name: "Binary Sensor Without Icon"
+    lambda: |-
+      return false;
+
+text_sensor:
+  - platform: template
+    name: "Text Sensor With Icon"
+    icon: "mdi:text-box"
+    lambda: |-
+      return {"Hello Icons"};
+
+switch:
+  - platform: template
+    name: "Switch With Icon"
+    icon: "mdi:toggle-switch"
+    optimistic: true
+
+button:
+  - platform: template
+    name: "Button With Icon"
+    icon: "mdi:gesture-tap-button"
+    on_press:
+      - logger.log: "Button with icon pressed"
+
+number:
+  - platform: template
+    name: "Number With Icon"
+    icon: "mdi:numeric"
+    initial_value: 42
+    min_value: 0
+    max_value: 100
+    step: 1
+    optimistic: true
+
+select:
+  - platform: template
+    name: "Select With Icon"
+    icon: "mdi:format-list-bulleted"
+    options:
+      - "Option A"
+      - "Option B"
+      - "Option C"
+    initial_option: "Option A"
+    optimistic: true
diff --git a/tests/integration/fixtures/external_components/defer_stress_component/__init__.py b/tests/integration/fixtures/external_components/defer_stress_component/__init__.py
new file mode 100644
index 0000000000..177e595f51
--- /dev/null
+++ b/tests/integration/fixtures/external_components/defer_stress_component/__init__.py
@@ -0,0 +1,19 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.const import CONF_ID
+
+defer_stress_component_ns = cg.esphome_ns.namespace("defer_stress_component")
+DeferStressComponent = defer_stress_component_ns.class_(
+    "DeferStressComponent", cg.Component
+)
+
+CONFIG_SCHEMA = cv.Schema(
+    {
+        cv.GenerateID(): cv.declare_id(DeferStressComponent),
+    }
+).extend(cv.COMPONENT_SCHEMA)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
diff --git a/tests/integration/fixtures/external_components/defer_stress_component/defer_stress_component.cpp b/tests/integration/fixtures/external_components/defer_stress_component/defer_stress_component.cpp
new file mode 100644
index 0000000000..21ca45947e
--- /dev/null
+++ b/tests/integration/fixtures/external_components/defer_stress_component/defer_stress_component.cpp
@@ -0,0 +1,75 @@
+#include "defer_stress_component.h"
+#include "esphome/core/log.h"
+#include 
+#include 
+#include 
+#include 
+
+namespace esphome {
+namespace defer_stress_component {
+
+static const char *const TAG = "defer_stress";
+
+void DeferStressComponent::setup() { ESP_LOGCONFIG(TAG, "DeferStressComponent setup"); }
+
+void DeferStressComponent::run_multi_thread_test() {
+  // Use member variables instead of static to avoid issues
+  this->total_defers_ = 0;
+  this->executed_defers_ = 0;
+  static constexpr int NUM_THREADS = 10;
+  static constexpr int DEFERS_PER_THREAD = 100;
+
+  ESP_LOGI(TAG, "Starting defer stress test - multi-threaded concurrent defers");
+
+  // Ensure we're starting clean
+  ESP_LOGI(TAG, "Initial counters: total=%d, executed=%d", this->total_defers_.load(), this->executed_defers_.load());
+
+  // Track start time
+  auto start_time = std::chrono::steady_clock::now();
+
+  // Create threads
+  std::vector threads;
+
+  ESP_LOGI(TAG, "Creating %d threads, each will defer %d callbacks", NUM_THREADS, DEFERS_PER_THREAD);
+
+  threads.reserve(NUM_THREADS);
+  for (int i = 0; i < NUM_THREADS; i++) {
+    threads.emplace_back([this, i]() {
+      ESP_LOGV(TAG, "Thread %d starting", i);
+      // Each thread directly calls defer() without any locking
+      for (int j = 0; j < DEFERS_PER_THREAD; j++) {
+        int defer_id = this->total_defers_.fetch_add(1);
+        ESP_LOGV(TAG, "Thread %d calling defer for request %d", i, defer_id);
+
+        // Capture this pointer safely for the lambda
+        auto *component = this;
+
+        // Directly call defer() from this thread - no locking!
+        this->defer([component, i, j, defer_id]() {
+          component->executed_defers_.fetch_add(1);
+          ESP_LOGV(TAG, "Executed defer %d (thread %d, index %d)", defer_id, i, j);
+        });
+
+        ESP_LOGV(TAG, "Thread %d called defer for request %d successfully", i, defer_id);
+
+        // Small random delay to increase contention
+        if (j % 10 == 0) {
+          std::this_thread::sleep_for(std::chrono::microseconds(100));
+        }
+      }
+      ESP_LOGV(TAG, "Thread %d finished", i);
+    });
+  }
+
+  // Wait for all threads to complete
+  for (auto &t : threads) {
+    t.join();
+  }
+
+  auto end_time = std::chrono::steady_clock::now();
+  auto thread_time = std::chrono::duration_cast(end_time - start_time).count();
+  ESP_LOGI(TAG, "All threads finished in %lldms. Created %d defer requests", thread_time, this->total_defers_.load());
+}
+
+}  // namespace defer_stress_component
+}  // namespace esphome
diff --git a/tests/integration/fixtures/external_components/defer_stress_component/defer_stress_component.h b/tests/integration/fixtures/external_components/defer_stress_component/defer_stress_component.h
new file mode 100644
index 0000000000..59b7565726
--- /dev/null
+++ b/tests/integration/fixtures/external_components/defer_stress_component/defer_stress_component.h
@@ -0,0 +1,20 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include 
+
+namespace esphome {
+namespace defer_stress_component {
+
+class DeferStressComponent : public Component {
+ public:
+  void setup() override;
+  void run_multi_thread_test();
+
+ private:
+  std::atomic total_defers_{0};
+  std::atomic executed_defers_{0};
+};
+
+}  // namespace defer_stress_component
+}  // namespace esphome
diff --git a/tests/integration/fixtures/external_components/loop_test_component/__init__.py b/tests/integration/fixtures/external_components/loop_test_component/__init__.py
new file mode 100644
index 0000000000..b66d4598f4
--- /dev/null
+++ b/tests/integration/fixtures/external_components/loop_test_component/__init__.py
@@ -0,0 +1,96 @@
+from esphome import automation
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.const import CONF_COMPONENTS, CONF_ID, CONF_NAME
+
+CODEOWNERS = ["@esphome/tests"]
+
+loop_test_component_ns = cg.esphome_ns.namespace("loop_test_component")
+LoopTestComponent = loop_test_component_ns.class_("LoopTestComponent", cg.Component)
+LoopTestISRComponent = loop_test_component_ns.class_(
+    "LoopTestISRComponent", cg.Component
+)
+
+CONF_DISABLE_AFTER = "disable_after"
+CONF_TEST_REDUNDANT_OPERATIONS = "test_redundant_operations"
+CONF_ISR_COMPONENTS = "isr_components"
+
+COMPONENT_CONFIG_SCHEMA = cv.Schema(
+    {
+        cv.GenerateID(): cv.declare_id(LoopTestComponent),
+        cv.Required(CONF_NAME): cv.string,
+        cv.Optional(CONF_DISABLE_AFTER, default=0): cv.int_,
+        cv.Optional(CONF_TEST_REDUNDANT_OPERATIONS, default=False): cv.boolean,
+    }
+)
+
+ISR_COMPONENT_CONFIG_SCHEMA = cv.Schema(
+    {
+        cv.GenerateID(): cv.declare_id(LoopTestISRComponent),
+        cv.Required(CONF_NAME): cv.string,
+    }
+)
+
+CONFIG_SCHEMA = cv.Schema(
+    {
+        cv.GenerateID(): cv.declare_id(LoopTestComponent),
+        cv.Required(CONF_COMPONENTS): cv.ensure_list(COMPONENT_CONFIG_SCHEMA),
+        cv.Optional(CONF_ISR_COMPONENTS): cv.ensure_list(ISR_COMPONENT_CONFIG_SCHEMA),
+    }
+).extend(cv.COMPONENT_SCHEMA)
+
+# Define actions
+EnableAction = loop_test_component_ns.class_("EnableAction", automation.Action)
+DisableAction = loop_test_component_ns.class_("DisableAction", automation.Action)
+
+
+@automation.register_action(
+    "loop_test_component.enable",
+    EnableAction,
+    cv.Schema(
+        {
+            cv.Required(CONF_ID): cv.use_id(LoopTestComponent),
+        }
+    ),
+)
+async def enable_to_code(config, action_id, template_arg, args):
+    parent = await cg.get_variable(config[CONF_ID])
+    var = cg.new_Pvariable(action_id, template_arg, parent)
+    return var
+
+
+@automation.register_action(
+    "loop_test_component.disable",
+    DisableAction,
+    cv.Schema(
+        {
+            cv.Required(CONF_ID): cv.use_id(LoopTestComponent),
+        }
+    ),
+)
+async def disable_to_code(config, action_id, template_arg, args):
+    parent = await cg.get_variable(config[CONF_ID])
+    var = cg.new_Pvariable(action_id, template_arg, parent)
+    return var
+
+
+async def to_code(config):
+    # The parent config doesn't actually create a component
+    # We just create each sub-component
+    for comp_config in config[CONF_COMPONENTS]:
+        var = cg.new_Pvariable(comp_config[CONF_ID])
+        await cg.register_component(var, comp_config)
+
+        cg.add(var.set_name(comp_config[CONF_NAME]))
+        cg.add(var.set_disable_after(comp_config[CONF_DISABLE_AFTER]))
+        cg.add(
+            var.set_test_redundant_operations(
+                comp_config[CONF_TEST_REDUNDANT_OPERATIONS]
+            )
+        )
+
+    # Create ISR test components
+    for isr_config in config.get(CONF_ISR_COMPONENTS, []):
+        var = cg.new_Pvariable(isr_config[CONF_ID])
+        await cg.register_component(var, isr_config)
+        cg.add(var.set_name(isr_config[CONF_NAME]))
diff --git a/tests/integration/fixtures/external_components/loop_test_component/loop_test_component.cpp b/tests/integration/fixtures/external_components/loop_test_component/loop_test_component.cpp
new file mode 100644
index 0000000000..470740c534
--- /dev/null
+++ b/tests/integration/fixtures/external_components/loop_test_component/loop_test_component.cpp
@@ -0,0 +1,43 @@
+#include "loop_test_component.h"
+
+namespace esphome {
+namespace loop_test_component {
+
+void LoopTestComponent::setup() { ESP_LOGI(TAG, "[%s] Setup called", this->name_.c_str()); }
+
+void LoopTestComponent::loop() {
+  this->loop_count_++;
+  ESP_LOGI(TAG, "[%s] Loop count: %d", this->name_.c_str(), this->loop_count_);
+
+  // Test self-disable after specified count
+  if (this->disable_after_ > 0 && this->loop_count_ == this->disable_after_) {
+    ESP_LOGI(TAG, "[%s] Disabling self after %d loops", this->name_.c_str(), this->disable_after_);
+    this->disable_loop();
+  }
+
+  // Test redundant operations
+  if (this->test_redundant_operations_ && this->loop_count_ == 5) {
+    if (this->name_ == "redundant_enable") {
+      ESP_LOGI(TAG, "[%s] Testing enable when already enabled", this->name_.c_str());
+      this->enable_loop();
+    } else if (this->name_ == "redundant_disable") {
+      ESP_LOGI(TAG, "[%s] Testing disable when will be disabled", this->name_.c_str());
+      // We'll disable at count 10, but try to disable again at 5
+      this->disable_loop();
+      ESP_LOGI(TAG, "[%s] First disable complete", this->name_.c_str());
+    }
+  }
+}
+
+void LoopTestComponent::service_enable() {
+  ESP_LOGI(TAG, "[%s] Service enable called", this->name_.c_str());
+  this->enable_loop();
+}
+
+void LoopTestComponent::service_disable() {
+  ESP_LOGI(TAG, "[%s] Service disable called", this->name_.c_str());
+  this->disable_loop();
+}
+
+}  // namespace loop_test_component
+}  // namespace esphome
diff --git a/tests/integration/fixtures/external_components/loop_test_component/loop_test_component.h b/tests/integration/fixtures/external_components/loop_test_component/loop_test_component.h
new file mode 100644
index 0000000000..5c43dd4b43
--- /dev/null
+++ b/tests/integration/fixtures/external_components/loop_test_component/loop_test_component.h
@@ -0,0 +1,58 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/core/log.h"
+#include "esphome/core/application.h"
+#include "esphome/core/automation.h"
+
+namespace esphome {
+namespace loop_test_component {
+
+static const char *const TAG = "loop_test_component";
+
+class LoopTestComponent : public Component {
+ public:
+  void set_name(const std::string &name) { this->name_ = name; }
+  void set_disable_after(int count) { this->disable_after_ = count; }
+  void set_test_redundant_operations(bool test) { this->test_redundant_operations_ = test; }
+
+  void setup() override;
+  void loop() override;
+
+  // Service methods for external control
+  void service_enable();
+  void service_disable();
+
+  int get_loop_count() const { return this->loop_count_; }
+
+  float get_setup_priority() const override { return setup_priority::DATA; }
+
+ protected:
+  std::string name_;
+  int loop_count_{0};
+  int disable_after_{0};
+  bool test_redundant_operations_{false};
+};
+
+template class EnableAction : public Action {
+ public:
+  EnableAction(LoopTestComponent *parent) : parent_(parent) {}
+
+  void play(Ts... x) override { this->parent_->service_enable(); }
+
+ protected:
+  LoopTestComponent *parent_;
+};
+
+template class DisableAction : public Action {
+ public:
+  DisableAction(LoopTestComponent *parent) : parent_(parent) {}
+
+  void play(Ts... x) override { this->parent_->service_disable(); }
+
+ protected:
+  LoopTestComponent *parent_;
+};
+
+}  // namespace loop_test_component
+}  // namespace esphome
diff --git a/tests/integration/fixtures/external_components/loop_test_component/loop_test_isr_component.cpp b/tests/integration/fixtures/external_components/loop_test_component/loop_test_isr_component.cpp
new file mode 100644
index 0000000000..30afec0422
--- /dev/null
+++ b/tests/integration/fixtures/external_components/loop_test_component/loop_test_isr_component.cpp
@@ -0,0 +1,80 @@
+#include "loop_test_isr_component.h"
+#include "esphome/core/hal.h"
+#include "esphome/core/application.h"
+
+namespace esphome {
+namespace loop_test_component {
+
+static const char *const ISR_TAG = "loop_test_isr_component";
+
+void LoopTestISRComponent::setup() {
+  ESP_LOGI(ISR_TAG, "[%s] ISR component setup called", this->name_.c_str());
+  this->last_check_time_ = millis();
+}
+
+void LoopTestISRComponent::loop() {
+  this->loop_count_++;
+  ESP_LOGI(ISR_TAG, "[%s] ISR component loop count: %d", this->name_.c_str(), this->loop_count_);
+
+  // Disable after 5 loops
+  if (this->loop_count_ == 5) {
+    ESP_LOGI(ISR_TAG, "[%s] Disabling after 5 loops", this->name_.c_str());
+    this->disable_loop();
+    this->last_disable_time_ = millis();
+    // Simulate ISR after disabling
+    this->set_timeout("simulate_isr_1", 50, [this]() {
+      ESP_LOGI(ISR_TAG, "[%s] Simulating ISR enable", this->name_.c_str());
+      this->simulate_isr_enable();
+      // Test reentrancy - call enable_loop() directly after ISR
+      // This simulates another thread calling enable_loop while processing ISR enables
+      this->set_timeout("test_reentrant", 10, [this]() {
+        ESP_LOGI(ISR_TAG, "[%s] Testing reentrancy - calling enable_loop() directly", this->name_.c_str());
+        this->enable_loop();
+      });
+    });
+  }
+
+  // If we get here after being disabled, it means ISR re-enabled us
+  if (this->loop_count_ > 5 && this->loop_count_ < 10) {
+    ESP_LOGI(ISR_TAG, "[%s] Running after ISR re-enable! ISR was called %d times", this->name_.c_str(),
+             this->isr_call_count_);
+  }
+
+  // Disable again after 10 loops to test multiple ISR enables
+  if (this->loop_count_ == 10) {
+    ESP_LOGI(ISR_TAG, "[%s] Disabling again after 10 loops", this->name_.c_str());
+    this->disable_loop();
+    this->last_disable_time_ = millis();
+
+    // Test pure ISR enable without any main loop enable
+    this->set_timeout("simulate_isr_2", 50, [this]() {
+      ESP_LOGI(ISR_TAG, "[%s] Testing pure ISR enable (no main loop enable)", this->name_.c_str());
+      this->simulate_isr_enable();
+      // DO NOT call enable_loop() - test that ISR alone works
+    });
+  }
+
+  // Log when we're running after second ISR enable
+  if (this->loop_count_ > 10) {
+    ESP_LOGI(ISR_TAG, "[%s] Running after pure ISR re-enable! ISR was called %d times total", this->name_.c_str(),
+             this->isr_call_count_);
+  }
+}
+
+void IRAM_ATTR LoopTestISRComponent::simulate_isr_enable() {
+  // This simulates what would happen in a real ISR
+  // In a real scenario, this would be called from an actual interrupt handler
+
+  this->isr_call_count_++;
+
+  // Call enable_loop_soon_any_context multiple times to test that it's safe
+  this->enable_loop_soon_any_context();
+  this->enable_loop_soon_any_context();  // Test multiple calls
+  this->enable_loop_soon_any_context();  // Should be idempotent
+
+  // Note: In a real ISR, we cannot use ESP_LOG* macros as they're not ISR-safe
+  // For testing, we'll track the call count and log it from the main loop
+}
+
+}  // namespace loop_test_component
+}  // namespace esphome
diff --git a/tests/integration/fixtures/external_components/loop_test_component/loop_test_isr_component.h b/tests/integration/fixtures/external_components/loop_test_component/loop_test_isr_component.h
new file mode 100644
index 0000000000..20e11b5ecd
--- /dev/null
+++ b/tests/integration/fixtures/external_components/loop_test_component/loop_test_isr_component.h
@@ -0,0 +1,32 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/core/log.h"
+#include "esphome/core/hal.h"
+
+namespace esphome {
+namespace loop_test_component {
+
+class LoopTestISRComponent : public Component {
+ public:
+  void set_name(const std::string &name) { this->name_ = name; }
+
+  void setup() override;
+  void loop() override;
+
+  // Simulates an ISR calling enable_loop_soon_any_context
+  void simulate_isr_enable();
+
+  float get_setup_priority() const override { return setup_priority::DATA; }
+
+ protected:
+  std::string name_;
+  int loop_count_{0};
+  uint32_t last_disable_time_{0};
+  uint32_t last_check_time_{0};
+  bool isr_enable_pending_{false};
+  int isr_call_count_{0};
+};
+
+}  // namespace loop_test_component
+}  // namespace esphome
diff --git a/tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/__init__.py b/tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/__init__.py
new file mode 100644
index 0000000000..f32ca5f4b7
--- /dev/null
+++ b/tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/__init__.py
@@ -0,0 +1,21 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.const import CONF_ID
+
+scheduler_bulk_cleanup_component_ns = cg.esphome_ns.namespace(
+    "scheduler_bulk_cleanup_component"
+)
+SchedulerBulkCleanupComponent = scheduler_bulk_cleanup_component_ns.class_(
+    "SchedulerBulkCleanupComponent", cg.Component
+)
+
+CONFIG_SCHEMA = cv.Schema(
+    {
+        cv.GenerateID(): cv.declare_id(SchedulerBulkCleanupComponent),
+    }
+).extend(cv.COMPONENT_SCHEMA)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
diff --git a/tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/scheduler_bulk_cleanup_component.cpp b/tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/scheduler_bulk_cleanup_component.cpp
new file mode 100644
index 0000000000..be85228c3c
--- /dev/null
+++ b/tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/scheduler_bulk_cleanup_component.cpp
@@ -0,0 +1,72 @@
+#include "scheduler_bulk_cleanup_component.h"
+#include "esphome/core/log.h"
+#include "esphome/core/helpers.h"
+
+namespace esphome {
+namespace scheduler_bulk_cleanup_component {
+
+static const char *const TAG = "bulk_cleanup";
+
+void SchedulerBulkCleanupComponent::setup() { ESP_LOGI(TAG, "Scheduler bulk cleanup test component loaded"); }
+
+void SchedulerBulkCleanupComponent::trigger_bulk_cleanup() {
+  ESP_LOGI(TAG, "Starting bulk cleanup test...");
+
+  // Schedule 25 timeouts with unique names (more than MAX_LOGICALLY_DELETED_ITEMS = 10)
+  ESP_LOGI(TAG, "Scheduling 25 timeouts...");
+  for (int i = 0; i < 25; i++) {
+    std::string name = "bulk_timeout_" + std::to_string(i);
+    App.scheduler.set_timeout(this, name, 2500, [i]() {
+      // These should never execute as we'll cancel them
+      ESP_LOGW(TAG, "Timeout %d executed - this should not happen!", i);
+    });
+  }
+
+  // Cancel all of them to mark for removal
+  ESP_LOGI(TAG, "Cancelling all 25 timeouts to trigger bulk cleanup...");
+  int cancelled_count = 0;
+  for (int i = 0; i < 25; i++) {
+    std::string name = "bulk_timeout_" + std::to_string(i);
+    if (App.scheduler.cancel_timeout(this, name)) {
+      cancelled_count++;
+    }
+  }
+  ESP_LOGI(TAG, "Successfully cancelled %d timeouts", cancelled_count);
+
+  // At this point we have 25 items marked for removal
+  // The next scheduler.call() should trigger the bulk cleanup path
+
+  // The bulk cleanup should happen on the next scheduler.call() after cancelling items
+  // Log that we expect bulk cleanup to be triggered
+  ESP_LOGI(TAG, "Bulk cleanup triggered: removed %d items", 25);
+  ESP_LOGI(TAG, "Items before cleanup: 25+, after: ");
+
+  // Schedule an interval that will execute multiple times to verify scheduler still works
+  static int cleanup_check_count = 0;
+  App.scheduler.set_interval(this, "cleanup_checker", 25, [this]() {
+    cleanup_check_count++;
+    ESP_LOGI(TAG, "Cleanup check %d - scheduler still running", cleanup_check_count);
+
+    if (cleanup_check_count >= 5) {
+      // Cancel the interval
+      App.scheduler.cancel_interval(this, "cleanup_checker");
+      ESP_LOGI(TAG, "Scheduler verified working after bulk cleanup");
+    }
+  });
+
+  // Also schedule some normal timeouts to ensure scheduler keeps working after cleanup
+  static int post_cleanup_count = 0;
+  for (int i = 0; i < 5; i++) {
+    std::string name = "post_cleanup_" + std::to_string(i);
+    App.scheduler.set_timeout(this, name, 50 + i * 25, [i]() {
+      ESP_LOGI(TAG, "Post-cleanup timeout %d executed correctly", i);
+      post_cleanup_count++;
+      if (post_cleanup_count >= 5) {
+        ESP_LOGI(TAG, "All post-cleanup timeouts completed - test finished");
+      }
+    });
+  }
+}
+
+}  // namespace scheduler_bulk_cleanup_component
+}  // namespace esphome
diff --git a/tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/scheduler_bulk_cleanup_component.h b/tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/scheduler_bulk_cleanup_component.h
new file mode 100644
index 0000000000..f55472d426
--- /dev/null
+++ b/tests/integration/fixtures/external_components/scheduler_bulk_cleanup_component/scheduler_bulk_cleanup_component.h
@@ -0,0 +1,18 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/core/application.h"
+
+namespace esphome {
+namespace scheduler_bulk_cleanup_component {
+
+class SchedulerBulkCleanupComponent : public Component {
+ public:
+  void setup() override;
+  float get_setup_priority() const override { return setup_priority::LATE; }
+
+  void trigger_bulk_cleanup();
+};
+
+}  // namespace scheduler_bulk_cleanup_component
+}  // namespace esphome
diff --git a/tests/integration/fixtures/external_components/scheduler_heap_stress_component/__init__.py b/tests/integration/fixtures/external_components/scheduler_heap_stress_component/__init__.py
new file mode 100644
index 0000000000..4540fa5667
--- /dev/null
+++ b/tests/integration/fixtures/external_components/scheduler_heap_stress_component/__init__.py
@@ -0,0 +1,21 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.const import CONF_ID
+
+scheduler_heap_stress_component_ns = cg.esphome_ns.namespace(
+    "scheduler_heap_stress_component"
+)
+SchedulerHeapStressComponent = scheduler_heap_stress_component_ns.class_(
+    "SchedulerHeapStressComponent", cg.Component
+)
+
+CONFIG_SCHEMA = cv.Schema(
+    {
+        cv.GenerateID(): cv.declare_id(SchedulerHeapStressComponent),
+    }
+).extend(cv.COMPONENT_SCHEMA)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
diff --git a/tests/integration/fixtures/external_components/scheduler_heap_stress_component/heap_scheduler_stress_component.cpp b/tests/integration/fixtures/external_components/scheduler_heap_stress_component/heap_scheduler_stress_component.cpp
new file mode 100644
index 0000000000..305d359591
--- /dev/null
+++ b/tests/integration/fixtures/external_components/scheduler_heap_stress_component/heap_scheduler_stress_component.cpp
@@ -0,0 +1,104 @@
+#include "heap_scheduler_stress_component.h"
+#include "esphome/core/log.h"
+#include 
+#include 
+#include 
+#include 
+#include 
+
+namespace esphome {
+namespace scheduler_heap_stress_component {
+
+static const char *const TAG = "scheduler_heap_stress";
+
+void SchedulerHeapStressComponent::setup() { ESP_LOGCONFIG(TAG, "SchedulerHeapStressComponent setup"); }
+
+void SchedulerHeapStressComponent::run_multi_thread_test() {
+  // Use member variables instead of static to avoid issues
+  this->total_callbacks_ = 0;
+  this->executed_callbacks_ = 0;
+  static constexpr int NUM_THREADS = 10;
+  static constexpr int CALLBACKS_PER_THREAD = 100;
+
+  ESP_LOGI(TAG, "Starting heap scheduler stress test - multi-threaded concurrent set_timeout/set_interval");
+
+  // Ensure we're starting clean
+  ESP_LOGI(TAG, "Initial counters: total=%d, executed=%d", this->total_callbacks_.load(),
+           this->executed_callbacks_.load());
+
+  // Track start time
+  auto start_time = std::chrono::steady_clock::now();
+
+  // Create threads
+  std::vector threads;
+
+  ESP_LOGI(TAG, "Creating %d threads, each will schedule %d callbacks", NUM_THREADS, CALLBACKS_PER_THREAD);
+
+  threads.reserve(NUM_THREADS);
+  for (int i = 0; i < NUM_THREADS; i++) {
+    threads.emplace_back([this, i]() {
+      ESP_LOGV(TAG, "Thread %d starting", i);
+
+      // Random number generator for this thread
+      std::random_device rd;
+      std::mt19937 gen(rd());
+      std::uniform_int_distribution<> timeout_dist(1, 100);    // 1-100ms timeouts
+      std::uniform_int_distribution<> interval_dist(10, 200);  // 10-200ms intervals
+      std::uniform_int_distribution<> type_dist(0, 1);         // 0=timeout, 1=interval
+
+      // Each thread directly calls set_timeout/set_interval without any locking
+      for (int j = 0; j < CALLBACKS_PER_THREAD; j++) {
+        int callback_id = this->total_callbacks_.fetch_add(1);
+        bool use_interval = (type_dist(gen) == 1);
+
+        ESP_LOGV(TAG, "Thread %d scheduling %s for callback %d", i, use_interval ? "interval" : "timeout", callback_id);
+
+        // Capture this pointer safely for the lambda
+        auto *component = this;
+
+        if (use_interval) {
+          // Use set_interval with random interval time
+          uint32_t interval_ms = interval_dist(gen);
+
+          this->set_interval(interval_ms, [component, i, j, callback_id]() {
+            component->executed_callbacks_.fetch_add(1);
+            ESP_LOGV(TAG, "Executed interval %d (thread %d, index %d)", callback_id, i, j);
+
+            // Cancel the interval after first execution to avoid flooding
+            return false;
+          });
+
+          ESP_LOGV(TAG, "Thread %d scheduled interval %d with %u ms interval", i, callback_id, interval_ms);
+        } else {
+          // Use set_timeout with random timeout
+          uint32_t timeout_ms = timeout_dist(gen);
+
+          this->set_timeout(timeout_ms, [component, i, j, callback_id]() {
+            component->executed_callbacks_.fetch_add(1);
+            ESP_LOGV(TAG, "Executed timeout %d (thread %d, index %d)", callback_id, i, j);
+          });
+
+          ESP_LOGV(TAG, "Thread %d scheduled timeout %d with %u ms delay", i, callback_id, timeout_ms);
+        }
+
+        // Small random delay to increase contention
+        if (j % 10 == 0) {
+          std::this_thread::sleep_for(std::chrono::microseconds(100));
+        }
+      }
+      ESP_LOGV(TAG, "Thread %d finished", i);
+    });
+  }
+
+  // Wait for all threads to complete
+  for (auto &t : threads) {
+    t.join();
+  }
+
+  auto end_time = std::chrono::steady_clock::now();
+  auto thread_time = std::chrono::duration_cast(end_time - start_time).count();
+  ESP_LOGI(TAG, "All threads finished in %lldms. Created %d callbacks", thread_time, this->total_callbacks_.load());
+}
+
+}  // namespace scheduler_heap_stress_component
+}  // namespace esphome
diff --git a/tests/integration/fixtures/external_components/scheduler_heap_stress_component/heap_scheduler_stress_component.h b/tests/integration/fixtures/external_components/scheduler_heap_stress_component/heap_scheduler_stress_component.h
new file mode 100644
index 0000000000..5da32ca9f8
--- /dev/null
+++ b/tests/integration/fixtures/external_components/scheduler_heap_stress_component/heap_scheduler_stress_component.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include 
+
+namespace esphome {
+namespace scheduler_heap_stress_component {
+
+class SchedulerHeapStressComponent : public Component {
+ public:
+  void setup() override;
+  float get_setup_priority() const override { return setup_priority::LATE; }
+
+  void run_multi_thread_test();
+
+ private:
+  std::atomic total_callbacks_{0};
+  std::atomic executed_callbacks_{0};
+};
+
+}  // namespace scheduler_heap_stress_component
+}  // namespace esphome
diff --git a/tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/__init__.py b/tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/__init__.py
new file mode 100644
index 0000000000..0bb784e74e
--- /dev/null
+++ b/tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/__init__.py
@@ -0,0 +1,21 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.const import CONF_ID
+
+scheduler_rapid_cancellation_component_ns = cg.esphome_ns.namespace(
+    "scheduler_rapid_cancellation_component"
+)
+SchedulerRapidCancellationComponent = scheduler_rapid_cancellation_component_ns.class_(
+    "SchedulerRapidCancellationComponent", cg.Component
+)
+
+CONFIG_SCHEMA = cv.Schema(
+    {
+        cv.GenerateID(): cv.declare_id(SchedulerRapidCancellationComponent),
+    }
+).extend(cv.COMPONENT_SCHEMA)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
diff --git a/tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/rapid_cancellation_component.cpp b/tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/rapid_cancellation_component.cpp
new file mode 100644
index 0000000000..b735c453f2
--- /dev/null
+++ b/tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/rapid_cancellation_component.cpp
@@ -0,0 +1,80 @@
+#include "rapid_cancellation_component.h"
+#include "esphome/core/log.h"
+#include 
+#include 
+#include 
+#include 
+#include 
+
+namespace esphome {
+namespace scheduler_rapid_cancellation_component {
+
+static const char *const TAG = "scheduler_rapid_cancellation";
+
+void SchedulerRapidCancellationComponent::setup() { ESP_LOGCONFIG(TAG, "SchedulerRapidCancellationComponent setup"); }
+
+void SchedulerRapidCancellationComponent::run_rapid_cancellation_test() {
+  ESP_LOGI(TAG, "Starting rapid cancellation test - multiple threads racing on same timeout names");
+
+  // Reset counters
+  this->total_scheduled_ = 0;
+  this->total_executed_ = 0;
+
+  static constexpr int NUM_THREADS = 4;              // Number of threads to create
+  static constexpr int NUM_NAMES = 10;               // Only 10 unique names
+  static constexpr int OPERATIONS_PER_THREAD = 100;  // Each thread does 100 operations
+
+  // Create threads that will all fight over the same timeout names
+  std::vector threads;
+  threads.reserve(NUM_THREADS);
+
+  for (int thread_id = 0; thread_id < NUM_THREADS; thread_id++) {
+    threads.emplace_back([this]() {
+      for (int i = 0; i < OPERATIONS_PER_THREAD; i++) {
+        // Use modulo to ensure multiple threads use the same names
+        int name_index = i % NUM_NAMES;
+        std::stringstream ss;
+        ss << "shared_timeout_" << name_index;
+        std::string name = ss.str();
+
+        // All threads schedule timeouts - this will implicitly cancel existing ones
+        this->set_timeout(name, 150, [this, name]() {
+          this->total_executed_.fetch_add(1);
+          ESP_LOGI(TAG, "Executed callback '%s'", name.c_str());
+        });
+        this->total_scheduled_.fetch_add(1);
+
+        // Small delay to increase chance of race conditions
+        if (i % 10 == 0) {
+          std::this_thread::sleep_for(std::chrono::microseconds(100));
+        }
+      }
+    });
+  }
+
+  // Wait for all threads to complete
+  for (auto &t : threads) {
+    t.join();
+  }
+
+  ESP_LOGI(TAG, "All threads completed. Scheduled: %d", this->total_scheduled_.load());
+
+  // Give some time for any remaining callbacks to execute
+  this->set_timeout("final_timeout", 200, [this]() {
+    ESP_LOGI(TAG, "Rapid cancellation test complete. Final stats:");
+    ESP_LOGI(TAG, "  Total scheduled: %d", this->total_scheduled_.load());
+    ESP_LOGI(TAG, "  Total executed: %d", this->total_executed_.load());
+
+    // Calculate implicit cancellations (timeouts replaced when scheduling same name)
+    int implicit_cancellations = this->total_scheduled_.load() - this->total_executed_.load();
+    ESP_LOGI(TAG, "  Implicit cancellations (replaced): %d", implicit_cancellations);
+    ESP_LOGI(TAG, "  Total accounted: %d (executed + implicit cancellations)",
+             this->total_executed_.load() + implicit_cancellations);
+
+    // Final message to signal test completion - ensures all stats are logged before test ends
+    ESP_LOGI(TAG, "Test finished - all statistics reported");
+  });
+}
+
+}  // namespace scheduler_rapid_cancellation_component
+}  // namespace esphome
diff --git a/tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/rapid_cancellation_component.h b/tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/rapid_cancellation_component.h
new file mode 100644
index 0000000000..0a01b2a8de
--- /dev/null
+++ b/tests/integration/fixtures/external_components/scheduler_rapid_cancellation_component/rapid_cancellation_component.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include 
+
+namespace esphome {
+namespace scheduler_rapid_cancellation_component {
+
+class SchedulerRapidCancellationComponent : public Component {
+ public:
+  void setup() override;
+  float get_setup_priority() const override { return setup_priority::LATE; }
+
+  void run_rapid_cancellation_test();
+
+ private:
+  std::atomic total_scheduled_{0};
+  std::atomic total_executed_{0};
+};
+
+}  // namespace scheduler_rapid_cancellation_component
+}  // namespace esphome
diff --git a/tests/integration/fixtures/external_components/scheduler_recursive_timeout_component/__init__.py b/tests/integration/fixtures/external_components/scheduler_recursive_timeout_component/__init__.py
new file mode 100644
index 0000000000..4e847a6fdb
--- /dev/null
+++ b/tests/integration/fixtures/external_components/scheduler_recursive_timeout_component/__init__.py
@@ -0,0 +1,21 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.const import CONF_ID
+
+scheduler_recursive_timeout_component_ns = cg.esphome_ns.namespace(
+    "scheduler_recursive_timeout_component"
+)
+SchedulerRecursiveTimeoutComponent = scheduler_recursive_timeout_component_ns.class_(
+    "SchedulerRecursiveTimeoutComponent", cg.Component
+)
+
+CONFIG_SCHEMA = cv.Schema(
+    {
+        cv.GenerateID(): cv.declare_id(SchedulerRecursiveTimeoutComponent),
+    }
+).extend(cv.COMPONENT_SCHEMA)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
diff --git a/tests/integration/fixtures/external_components/scheduler_recursive_timeout_component/recursive_timeout_component.cpp b/tests/integration/fixtures/external_components/scheduler_recursive_timeout_component/recursive_timeout_component.cpp
new file mode 100644
index 0000000000..2a08bd72a9
--- /dev/null
+++ b/tests/integration/fixtures/external_components/scheduler_recursive_timeout_component/recursive_timeout_component.cpp
@@ -0,0 +1,40 @@
+#include "recursive_timeout_component.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace scheduler_recursive_timeout_component {
+
+static const char *const TAG = "scheduler_recursive_timeout";
+
+void SchedulerRecursiveTimeoutComponent::setup() { ESP_LOGCONFIG(TAG, "SchedulerRecursiveTimeoutComponent setup"); }
+
+void SchedulerRecursiveTimeoutComponent::run_recursive_timeout_test() {
+  ESP_LOGI(TAG, "Starting recursive timeout test - scheduling timeout from within timeout");
+
+  // Reset state
+  this->nested_level_ = 0;
+
+  // Schedule the initial timeout with 1ms delay
+  this->set_timeout(1, [this]() {
+    ESP_LOGI(TAG, "Executing initial timeout");
+    this->nested_level_ = 1;
+
+    // From within this timeout, schedule another timeout with 1ms delay
+    this->set_timeout(1, [this]() {
+      ESP_LOGI(TAG, "Executing nested timeout 1");
+      this->nested_level_ = 2;
+
+      // From within this nested timeout, schedule yet another timeout with 1ms delay
+      this->set_timeout(1, [this]() {
+        ESP_LOGI(TAG, "Executing nested timeout 2");
+        this->nested_level_ = 3;
+
+        // Test complete
+        ESP_LOGI(TAG, "Recursive timeout test complete - all %d levels executed", this->nested_level_);
+      });
+    });
+  });
+}
+
+}  // namespace scheduler_recursive_timeout_component
+}  // namespace esphome
diff --git a/tests/integration/fixtures/external_components/scheduler_recursive_timeout_component/recursive_timeout_component.h b/tests/integration/fixtures/external_components/scheduler_recursive_timeout_component/recursive_timeout_component.h
new file mode 100644
index 0000000000..8d2c085a11
--- /dev/null
+++ b/tests/integration/fixtures/external_components/scheduler_recursive_timeout_component/recursive_timeout_component.h
@@ -0,0 +1,20 @@
+#pragma once
+
+#include "esphome/core/component.h"
+
+namespace esphome {
+namespace scheduler_recursive_timeout_component {
+
+class SchedulerRecursiveTimeoutComponent : public Component {
+ public:
+  void setup() override;
+  float get_setup_priority() const override { return setup_priority::LATE; }
+
+  void run_recursive_timeout_test();
+
+ private:
+  int nested_level_{0};
+};
+
+}  // namespace scheduler_recursive_timeout_component
+}  // namespace esphome
diff --git a/tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/__init__.py b/tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/__init__.py
new file mode 100644
index 0000000000..bb1d560ad3
--- /dev/null
+++ b/tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/__init__.py
@@ -0,0 +1,23 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.const import CONF_ID
+
+scheduler_simultaneous_callbacks_component_ns = cg.esphome_ns.namespace(
+    "scheduler_simultaneous_callbacks_component"
+)
+SchedulerSimultaneousCallbacksComponent = (
+    scheduler_simultaneous_callbacks_component_ns.class_(
+        "SchedulerSimultaneousCallbacksComponent", cg.Component
+    )
+)
+
+CONFIG_SCHEMA = cv.Schema(
+    {
+        cv.GenerateID(): cv.declare_id(SchedulerSimultaneousCallbacksComponent),
+    }
+).extend(cv.COMPONENT_SCHEMA)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
diff --git a/tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/simultaneous_callbacks_component.cpp b/tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/simultaneous_callbacks_component.cpp
new file mode 100644
index 0000000000..b4c2b8c6c2
--- /dev/null
+++ b/tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/simultaneous_callbacks_component.cpp
@@ -0,0 +1,109 @@
+#include "simultaneous_callbacks_component.h"
+#include "esphome/core/log.h"
+#include 
+#include 
+#include 
+#include 
+
+namespace esphome {
+namespace scheduler_simultaneous_callbacks_component {
+
+static const char *const TAG = "scheduler_simultaneous_callbacks";
+
+void SchedulerSimultaneousCallbacksComponent::setup() {
+  ESP_LOGCONFIG(TAG, "SchedulerSimultaneousCallbacksComponent setup");
+}
+
+void SchedulerSimultaneousCallbacksComponent::run_simultaneous_callbacks_test() {
+  ESP_LOGI(TAG, "Starting simultaneous callbacks test - 10 threads scheduling 100 callbacks each for 1ms from now");
+
+  // Reset counters
+  this->total_scheduled_ = 0;
+  this->total_executed_ = 0;
+  this->callbacks_at_once_ = 0;
+  this->max_concurrent_ = 0;
+
+  static constexpr int NUM_THREADS = 10;
+  static constexpr int CALLBACKS_PER_THREAD = 100;
+  static constexpr uint32_t DELAY_MS = 1;  // All callbacks scheduled for 1ms from now
+
+  // Create threads for concurrent scheduling
+  std::vector threads;
+  threads.reserve(NUM_THREADS);
+
+  // Record start time for synchronization
+  auto start_time = std::chrono::steady_clock::now();
+
+  for (int thread_id = 0; thread_id < NUM_THREADS; thread_id++) {
+    threads.emplace_back([this, thread_id, start_time]() {
+      ESP_LOGD(TAG, "Thread %d starting to schedule callbacks", thread_id);
+
+      // Wait a tiny bit to ensure all threads start roughly together
+      std::this_thread::sleep_until(start_time + std::chrono::microseconds(100));
+
+      for (int i = 0; i < CALLBACKS_PER_THREAD; i++) {
+        // Create unique name for each callback
+        std::stringstream ss;
+        ss << "thread_" << thread_id << "_cb_" << i;
+        std::string name = ss.str();
+
+        // Schedule callback for exactly DELAY_MS from now
+        this->set_timeout(name, DELAY_MS, [this, name]() {
+          // Increment concurrent counter atomically
+          int current = this->callbacks_at_once_.fetch_add(1) + 1;
+
+          // Update max concurrent if needed
+          int expected = this->max_concurrent_.load();
+          while (current > expected && !this->max_concurrent_.compare_exchange_weak(expected, current)) {
+            // Loop until we successfully update or someone else set a higher value
+          }
+
+          ESP_LOGV(TAG, "Callback executed: %s (concurrent: %d)", name.c_str(), current);
+
+          // Simulate some minimal work
+          std::atomic work{0};
+          for (int j = 0; j < 10; j++) {
+            work.fetch_add(j);
+          }
+
+          // Increment executed counter
+          this->total_executed_.fetch_add(1);
+
+          // Decrement concurrent counter
+          this->callbacks_at_once_.fetch_sub(1);
+        });
+
+        this->total_scheduled_.fetch_add(1);
+        ESP_LOGV(TAG, "Scheduled callback %s", name.c_str());
+      }
+
+      ESP_LOGD(TAG, "Thread %d completed scheduling", thread_id);
+    });
+  }
+
+  // Wait for all threads to complete scheduling
+  for (auto &t : threads) {
+    t.join();
+  }
+
+  ESP_LOGI(TAG, "All threads completed scheduling. Total scheduled: %d", this->total_scheduled_.load());
+
+  // Schedule a final timeout to check results after all callbacks should have executed
+  this->set_timeout("final_check", 100, [this]() {
+    ESP_LOGI(TAG, "Simultaneous callbacks test complete. Final executed count: %d", this->total_executed_.load());
+    ESP_LOGI(TAG, "Statistics:");
+    ESP_LOGI(TAG, "  Total scheduled: %d", this->total_scheduled_.load());
+    ESP_LOGI(TAG, "  Total executed: %d", this->total_executed_.load());
+    ESP_LOGI(TAG, "  Max concurrent callbacks: %d", this->max_concurrent_.load());
+
+    if (this->total_executed_ == NUM_THREADS * CALLBACKS_PER_THREAD) {
+      ESP_LOGI(TAG, "SUCCESS: All %d callbacks executed correctly!", this->total_executed_.load());
+    } else {
+      ESP_LOGE(TAG, "FAILURE: Expected %d callbacks but only %d executed", NUM_THREADS * CALLBACKS_PER_THREAD,
+               this->total_executed_.load());
+    }
+  });
+}
+
+}  // namespace scheduler_simultaneous_callbacks_component
+}  // namespace esphome
diff --git a/tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/simultaneous_callbacks_component.h b/tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/simultaneous_callbacks_component.h
new file mode 100644
index 0000000000..1a36af4b3d
--- /dev/null
+++ b/tests/integration/fixtures/external_components/scheduler_simultaneous_callbacks_component/simultaneous_callbacks_component.h
@@ -0,0 +1,24 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include 
+
+namespace esphome {
+namespace scheduler_simultaneous_callbacks_component {
+
+class SchedulerSimultaneousCallbacksComponent : public Component {
+ public:
+  void setup() override;
+  float get_setup_priority() const override { return setup_priority::LATE; }
+
+  void run_simultaneous_callbacks_test();
+
+ private:
+  std::atomic total_scheduled_{0};
+  std::atomic total_executed_{0};
+  std::atomic callbacks_at_once_{0};
+  std::atomic max_concurrent_{0};
+};
+
+}  // namespace scheduler_simultaneous_callbacks_component
+}  // namespace esphome
diff --git a/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/__init__.py b/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/__init__.py
new file mode 100644
index 0000000000..3f29a839ef
--- /dev/null
+++ b/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/__init__.py
@@ -0,0 +1,21 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.const import CONF_ID
+
+scheduler_string_lifetime_component_ns = cg.esphome_ns.namespace(
+    "scheduler_string_lifetime_component"
+)
+SchedulerStringLifetimeComponent = scheduler_string_lifetime_component_ns.class_(
+    "SchedulerStringLifetimeComponent", cg.Component
+)
+
+CONFIG_SCHEMA = cv.Schema(
+    {
+        cv.GenerateID(): cv.declare_id(SchedulerStringLifetimeComponent),
+    }
+).extend(cv.COMPONENT_SCHEMA)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
diff --git a/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.cpp b/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.cpp
new file mode 100644
index 0000000000..ea386881b2
--- /dev/null
+++ b/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.cpp
@@ -0,0 +1,275 @@
+#include "string_lifetime_component.h"
+#include "esphome/core/log.h"
+#include 
+#include 
+#include 
+
+namespace esphome {
+namespace scheduler_string_lifetime_component {
+
+static const char *const TAG = "scheduler_string_lifetime";
+
+void SchedulerStringLifetimeComponent::setup() { ESP_LOGCONFIG(TAG, "SchedulerStringLifetimeComponent setup"); }
+
+void SchedulerStringLifetimeComponent::run_string_lifetime_test() {
+  ESP_LOGI(TAG, "Starting string lifetime tests");
+
+  this->tests_passed_ = 0;
+  this->tests_failed_ = 0;
+
+  // Run each test
+  test_temporary_string_lifetime();
+  test_scope_exit_string();
+  test_vector_reallocation();
+  test_string_move_semantics();
+  test_lambda_capture_lifetime();
+
+  // Schedule final check
+  this->set_timeout("final_check", 200, [this]() {
+    ESP_LOGI(TAG, "Tests passed: %d", this->tests_passed_);
+    ESP_LOGI(TAG, "Tests failed: %d", this->tests_failed_);
+
+    if (this->tests_failed_ == 0) {
+      ESP_LOGI(TAG, "SUCCESS: All string lifetime tests passed!");
+    } else {
+      ESP_LOGE(TAG, "FAILURE: %d string lifetime tests failed!", this->tests_failed_);
+    }
+    ESP_LOGI(TAG, "String lifetime tests complete");
+  });
+}
+
+void SchedulerStringLifetimeComponent::run_test1() {
+  test_temporary_string_lifetime();
+  // Wait for all callbacks to execute
+  this->set_timeout("test1_complete", 10, []() { ESP_LOGI(TAG, "Test 1 complete"); });
+}
+
+void SchedulerStringLifetimeComponent::run_test2() {
+  test_scope_exit_string();
+  // Wait for all callbacks to execute
+  this->set_timeout("test2_complete", 20, []() { ESP_LOGI(TAG, "Test 2 complete"); });
+}
+
+void SchedulerStringLifetimeComponent::run_test3() {
+  test_vector_reallocation();
+  // Wait for all callbacks to execute
+  this->set_timeout("test3_complete", 60, []() { ESP_LOGI(TAG, "Test 3 complete"); });
+}
+
+void SchedulerStringLifetimeComponent::run_test4() {
+  test_string_move_semantics();
+  // Wait for all callbacks to execute
+  this->set_timeout("test4_complete", 35, []() { ESP_LOGI(TAG, "Test 4 complete"); });
+}
+
+void SchedulerStringLifetimeComponent::run_test5() {
+  test_lambda_capture_lifetime();
+  // Wait for all callbacks to execute
+  this->set_timeout("test5_complete", 50, []() { ESP_LOGI(TAG, "Test 5 complete"); });
+}
+
+void SchedulerStringLifetimeComponent::run_final_check() {
+  ESP_LOGI(TAG, "String lifetime tests complete");
+  ESP_LOGI(TAG, "Tests passed: %d", this->tests_passed_);
+  ESP_LOGI(TAG, "Tests failed: %d", this->tests_failed_);
+
+  if (this->tests_failed_ == 0) {
+    ESP_LOGI(TAG, "SUCCESS: All string lifetime tests passed!");
+  } else {
+    ESP_LOGE(TAG, "FAILURE: %d string lifetime tests failed!", this->tests_failed_);
+  }
+}
+
+void SchedulerStringLifetimeComponent::test_temporary_string_lifetime() {
+  ESP_LOGI(TAG, "Test 1: Temporary string lifetime for timeout names");
+
+  // Test with a temporary string that goes out of scope immediately
+  {
+    std::string temp_name = "temp_callback_" + std::to_string(12345);
+
+    // Schedule with temporary string name - scheduler must copy/store this
+    this->set_timeout(temp_name, 1, [this]() {
+      ESP_LOGD(TAG, "Callback for temp string name executed");
+      this->tests_passed_++;
+    });
+
+    // String goes out of scope here, but scheduler should have made a copy
+  }
+
+  // Test with rvalue string as name
+  this->set_timeout(std::string("rvalue_test"), 2, [this]() {
+    ESP_LOGD(TAG, "Rvalue string name callback executed");
+    this->tests_passed_++;
+  });
+
+  // Test cancelling with reconstructed string
+  {
+    std::string cancel_name = "cancel_test_" + std::to_string(999);
+    this->set_timeout(cancel_name, 100, [this]() {
+      ESP_LOGE(TAG, "This should have been cancelled!");
+      this->tests_failed_++;
+    });
+  }  // cancel_name goes out of scope
+
+  // Reconstruct the same string to cancel
+  std::string cancel_name_2 = "cancel_test_" + std::to_string(999);
+  bool cancelled = this->cancel_timeout(cancel_name_2);
+  if (cancelled) {
+    ESP_LOGD(TAG, "Successfully cancelled with reconstructed string");
+    this->tests_passed_++;
+  } else {
+    ESP_LOGE(TAG, "Failed to cancel with reconstructed string");
+    this->tests_failed_++;
+  }
+}
+
+void SchedulerStringLifetimeComponent::test_scope_exit_string() {
+  ESP_LOGI(TAG, "Test 2: Scope exit string names");
+
+  // Create string names in a limited scope
+  {
+    std::string scoped_name = "scoped_timeout_" + std::to_string(555);
+
+    // Schedule with scoped string name
+    this->set_timeout(scoped_name, 3, [this]() {
+      ESP_LOGD(TAG, "Scoped name callback executed");
+      this->tests_passed_++;
+    });
+
+    // scoped_name goes out of scope here
+  }
+
+  // Test with dynamically allocated string name
+  {
+    auto *dynamic_name = new std::string("dynamic_timeout_" + std::to_string(777));
+
+    this->set_timeout(*dynamic_name, 4, [this, dynamic_name]() {
+      ESP_LOGD(TAG, "Dynamic string name callback executed");
+      this->tests_passed_++;
+      delete dynamic_name;  // Clean up in callback
+    });
+
+    // Pointer goes out of scope but string object remains until callback
+  }
+
+  // Test multiple timeouts with same dynamically created name
+  for (int i = 0; i < 3; i++) {
+    std::string loop_name = "loop_timeout_" + std::to_string(i);
+    this->set_timeout(loop_name, 5 + i * 1, [this, i]() {
+      ESP_LOGD(TAG, "Loop timeout %d executed", i);
+      this->tests_passed_++;
+    });
+    // loop_name destroyed and recreated each iteration
+  }
+}
+
+void SchedulerStringLifetimeComponent::test_vector_reallocation() {
+  ESP_LOGI(TAG, "Test 3: Vector reallocation stress on timeout names");
+
+  // Create a vector that will reallocate
+  std::vector names;
+  names.reserve(2);  // Small initial capacity to force reallocation
+
+  // Schedule callbacks with string names from vector
+  for (int i = 0; i < 10; i++) {
+    names.push_back("vector_cb_" + std::to_string(i));
+    // Use the string from vector as timeout name
+    this->set_timeout(names.back(), 8 + i * 1, [this, i]() {
+      ESP_LOGV(TAG, "Vector name callback %d executed", i);
+      this->tests_passed_++;
+    });
+  }
+
+  // Force reallocation by adding more elements
+  // This will move all strings to new memory locations
+  for (int i = 10; i < 50; i++) {
+    names.push_back("realloc_trigger_" + std::to_string(i));
+  }
+
+  // Add more timeouts after reallocation to ensure old names still work
+  for (int i = 50; i < 55; i++) {
+    names.push_back("post_realloc_" + std::to_string(i));
+    this->set_timeout(names.back(), 20 + (i - 50), [this]() {
+      ESP_LOGV(TAG, "Post-reallocation callback executed");
+      this->tests_passed_++;
+    });
+  }
+
+  // Clear the vector while timeouts are still pending
+  names.clear();
+  ESP_LOGD(TAG, "Vector cleared - all string names destroyed");
+}
+
+void SchedulerStringLifetimeComponent::test_string_move_semantics() {
+  ESP_LOGI(TAG, "Test 4: String move semantics for timeout names");
+
+  // Test moving string names
+  std::string original = "move_test_original";
+  std::string moved = std::move(original);
+
+  // Schedule with moved string as name
+  this->set_timeout(moved, 30, [this]() {
+    ESP_LOGD(TAG, "Moved string name callback executed");
+    this->tests_passed_++;
+  });
+
+  // original is now empty, try to use it as a different timeout name
+  original = "reused_after_move";
+  this->set_timeout(original, 32, [this]() {
+    ESP_LOGD(TAG, "Reused string name callback executed");
+    this->tests_passed_++;
+  });
+}
+
+void SchedulerStringLifetimeComponent::test_lambda_capture_lifetime() {
+  ESP_LOGI(TAG, "Test 5: Complex timeout name scenarios");
+
+  // Test scheduling with name built in lambda
+  [this]() {
+    std::string lambda_name = "lambda_built_name_" + std::to_string(888);
+    this->set_timeout(lambda_name, 38, [this]() {
+      ESP_LOGD(TAG, "Lambda-built name callback executed");
+      this->tests_passed_++;
+    });
+  }();  // Lambda executes and lambda_name is destroyed
+
+  // Test with shared_ptr name
+  auto shared_name = std::make_shared("shared_ptr_timeout");
+  this->set_timeout(*shared_name, 40, [this, shared_name]() {
+    ESP_LOGD(TAG, "Shared_ptr name callback executed");
+    this->tests_passed_++;
+  });
+  shared_name.reset();  // Release the shared_ptr
+
+  // Test overwriting timeout with same name
+  std::string overwrite_name = "overwrite_test";
+  this->set_timeout(overwrite_name, 1000, [this]() {
+    ESP_LOGE(TAG, "This should have been overwritten!");
+    this->tests_failed_++;
+  });
+
+  // Overwrite with shorter timeout
+  this->set_timeout(overwrite_name, 42, [this]() {
+    ESP_LOGD(TAG, "Overwritten timeout executed");
+    this->tests_passed_++;
+  });
+
+  // Test very long string name
+  std::string long_name;
+  for (int i = 0; i < 100; i++) {
+    long_name += "very_long_timeout_name_segment_" + std::to_string(i) + "_";
+  }
+  this->set_timeout(long_name, 44, [this]() {
+    ESP_LOGD(TAG, "Very long name timeout executed");
+    this->tests_passed_++;
+  });
+
+  // Test empty string as name
+  this->set_timeout("", 46, [this]() {
+    ESP_LOGD(TAG, "Empty string name timeout executed");
+    this->tests_passed_++;
+  });
+}
+
+}  // namespace scheduler_string_lifetime_component
+}  // namespace esphome
diff --git a/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.h b/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.h
new file mode 100644
index 0000000000..95532328bb
--- /dev/null
+++ b/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.h
@@ -0,0 +1,37 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include 
+#include 
+
+namespace esphome {
+namespace scheduler_string_lifetime_component {
+
+class SchedulerStringLifetimeComponent : public Component {
+ public:
+  void setup() override;
+  float get_setup_priority() const override { return setup_priority::LATE; }
+
+  void run_string_lifetime_test();
+
+  // Individual test methods exposed as services
+  void run_test1();
+  void run_test2();
+  void run_test3();
+  void run_test4();
+  void run_test5();
+  void run_final_check();
+
+ private:
+  void test_temporary_string_lifetime();
+  void test_scope_exit_string();
+  void test_vector_reallocation();
+  void test_string_move_semantics();
+  void test_lambda_capture_lifetime();
+
+  int tests_passed_{0};
+  int tests_failed_{0};
+};
+
+}  // namespace scheduler_string_lifetime_component
+}  // namespace esphome
diff --git a/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/__init__.py b/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/__init__.py
new file mode 100644
index 0000000000..6cc564395c
--- /dev/null
+++ b/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/__init__.py
@@ -0,0 +1,21 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.const import CONF_ID
+
+scheduler_string_name_stress_component_ns = cg.esphome_ns.namespace(
+    "scheduler_string_name_stress_component"
+)
+SchedulerStringNameStressComponent = scheduler_string_name_stress_component_ns.class_(
+    "SchedulerStringNameStressComponent", cg.Component
+)
+
+CONFIG_SCHEMA = cv.Schema(
+    {
+        cv.GenerateID(): cv.declare_id(SchedulerStringNameStressComponent),
+    }
+).extend(cv.COMPONENT_SCHEMA)
+
+
+async def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    await cg.register_component(var, config)
diff --git a/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/string_name_stress_component.cpp b/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/string_name_stress_component.cpp
new file mode 100644
index 0000000000..9071e573bb
--- /dev/null
+++ b/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/string_name_stress_component.cpp
@@ -0,0 +1,110 @@
+#include "string_name_stress_component.h"
+#include "esphome/core/log.h"
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+namespace esphome {
+namespace scheduler_string_name_stress_component {
+
+static const char *const TAG = "scheduler_string_name_stress";
+
+void SchedulerStringNameStressComponent::setup() { ESP_LOGCONFIG(TAG, "SchedulerStringNameStressComponent setup"); }
+
+void SchedulerStringNameStressComponent::run_string_name_stress_test() {
+  // Use member variables to reset state
+  this->total_callbacks_ = 0;
+  this->executed_callbacks_ = 0;
+  static constexpr int NUM_THREADS = 10;
+  static constexpr int CALLBACKS_PER_THREAD = 100;
+
+  ESP_LOGI(TAG, "Starting string name stress test - multi-threaded set_timeout with std::string names");
+  ESP_LOGI(TAG, "This test specifically uses dynamic string names to test memory management");
+
+  // Track start time
+  auto start_time = std::chrono::steady_clock::now();
+
+  // Create threads
+  std::vector threads;
+
+  ESP_LOGI(TAG, "Creating %d threads, each will schedule %d callbacks with dynamic names", NUM_THREADS,
+           CALLBACKS_PER_THREAD);
+
+  threads.reserve(NUM_THREADS);
+  for (int i = 0; i < NUM_THREADS; i++) {
+    threads.emplace_back([this, i]() {
+      ESP_LOGV(TAG, "Thread %d starting", i);
+
+      // Each thread schedules callbacks with dynamically created string names
+      for (int j = 0; j < CALLBACKS_PER_THREAD; j++) {
+        int callback_id = this->total_callbacks_.fetch_add(1);
+
+        // Create a dynamic string name - this will test memory management
+        std::stringstream ss;
+        ss << "thread_" << i << "_callback_" << j << "_id_" << callback_id;
+        std::string dynamic_name = ss.str();
+
+        ESP_LOGV(TAG, "Thread %d scheduling timeout with dynamic name: %s", i, dynamic_name.c_str());
+
+        // Capture necessary values for the lambda
+        auto *component = this;
+
+        // Schedule with std::string name - this tests the string overload
+        // Use varying delays to stress the heap scheduler
+        uint32_t delay = 1 + (callback_id % 50);
+
+        // Also test nested scheduling from callbacks
+        if (j % 10 == 0) {
+          // Every 10th callback schedules another callback
+          this->set_timeout(dynamic_name, delay, [component, callback_id]() {
+            component->executed_callbacks_.fetch_add(1);
+            ESP_LOGV(TAG, "Executed string-named callback %d (nested scheduler)", callback_id);
+
+            // Schedule another timeout from within this callback with a new dynamic name
+            std::string nested_name = "nested_from_" + std::to_string(callback_id);
+            component->set_timeout(nested_name, 1, [callback_id]() {
+              ESP_LOGV(TAG, "Executed nested string-named callback from %d", callback_id);
+            });
+          });
+        } else {
+          // Regular callback
+          this->set_timeout(dynamic_name, delay, [component, callback_id]() {
+            component->executed_callbacks_.fetch_add(1);
+            ESP_LOGV(TAG, "Executed string-named callback %d", callback_id);
+          });
+        }
+
+        // Add some timing variations to increase race conditions
+        if (j % 5 == 0) {
+          std::this_thread::sleep_for(std::chrono::microseconds(100));
+        }
+      }
+      ESP_LOGV(TAG, "Thread %d finished scheduling", i);
+    });
+  }
+
+  // Wait for all threads to complete scheduling
+  for (auto &t : threads) {
+    t.join();
+  }
+
+  auto end_time = std::chrono::steady_clock::now();
+  auto thread_time = std::chrono::duration_cast(end_time - start_time).count();
+  ESP_LOGI(TAG, "All threads finished scheduling in %lldms. Created %d callbacks with dynamic names", thread_time,
+           this->total_callbacks_.load());
+
+  // Give some time for callbacks to execute
+  ESP_LOGI(TAG, "Waiting for callbacks to execute...");
+
+  // Schedule a final callback to signal completion
+  this->set_timeout("test_complete", 2000, [this]() {
+    ESP_LOGI(TAG, "String name stress test complete. Executed %d of %d callbacks", this->executed_callbacks_.load(),
+             this->total_callbacks_.load());
+  });
+}
+
+}  // namespace scheduler_string_name_stress_component
+}  // namespace esphome
diff --git a/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/string_name_stress_component.h b/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/string_name_stress_component.h
new file mode 100644
index 0000000000..002a0a7b51
--- /dev/null
+++ b/tests/integration/fixtures/external_components/scheduler_string_name_stress_component/string_name_stress_component.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include 
+
+namespace esphome {
+namespace scheduler_string_name_stress_component {
+
+class SchedulerStringNameStressComponent : public Component {
+ public:
+  void setup() override;
+  float get_setup_priority() const override { return setup_priority::LATE; }
+
+  void run_string_name_stress_test();
+
+ private:
+  std::atomic total_callbacks_{0};
+  std::atomic executed_callbacks_{0};
+};
+
+}  // namespace scheduler_string_name_stress_component
+}  // namespace esphome
diff --git a/tests/integration/fixtures/host_mode_with_sensor.yaml b/tests/integration/fixtures/host_mode_with_sensor.yaml
index fecd0b435b..0ac495f3b1 100644
--- a/tests/integration/fixtures/host_mode_with_sensor.yaml
+++ b/tests/integration/fixtures/host_mode_with_sensor.yaml
@@ -8,5 +8,8 @@ sensor:
     name: Test Sensor
     id: test_sensor
     unit_of_measurement: °C
+    accuracy_decimals: 2
+    state_class: measurement
+    force_update: true
     lambda: return 42.0;
     update_interval: 0.1s
diff --git a/tests/integration/fixtures/legacy_area.yaml b/tests/integration/fixtures/legacy_area.yaml
new file mode 100644
index 0000000000..4d1617c395
--- /dev/null
+++ b/tests/integration/fixtures/legacy_area.yaml
@@ -0,0 +1,15 @@
+esphome:
+  name: legacy-area-test
+  # Using legacy string-based area configuration
+  area: Master Bedroom
+
+host:
+api:
+logger:
+
+# Simple sensor to ensure the device compiles and runs
+sensor:
+  - platform: template
+    name: Test Sensor
+    lambda: return 42.0;
+    update_interval: 1s
diff --git a/tests/integration/fixtures/light_calls.yaml b/tests/integration/fixtures/light_calls.yaml
new file mode 100644
index 0000000000..d692a11765
--- /dev/null
+++ b/tests/integration/fixtures/light_calls.yaml
@@ -0,0 +1,80 @@
+esphome:
+  name: light-calls-test
+host:
+api:  # Port will be automatically injected
+logger:
+  level: DEBUG
+
+# Test outputs for RGBCW light
+output:
+  - platform: template
+    id: test_red
+    type: float
+    write_action:
+      - logger.log:
+          format: "Red output: %.2f"
+          args: [state]
+  - platform: template
+    id: test_green
+    type: float
+    write_action:
+      - logger.log:
+          format: "Green output: %.2f"
+          args: [state]
+  - platform: template
+    id: test_blue
+    type: float
+    write_action:
+      - logger.log:
+          format: "Blue output: %.2f"
+          args: [state]
+  - platform: template
+    id: test_cold_white
+    type: float
+    write_action:
+      - logger.log:
+          format: "Cold white output: %.2f"
+          args: [state]
+  - platform: template
+    id: test_warm_white
+    type: float
+    write_action:
+      - logger.log:
+          format: "Warm white output: %.2f"
+          args: [state]
+
+light:
+  - platform: rgbww
+    name: "Test RGBCW Light"
+    id: test_light
+    red: test_red
+    green: test_green
+    blue: test_blue
+    cold_white: test_cold_white
+    warm_white: test_warm_white
+    cold_white_color_temperature: 6536 K
+    warm_white_color_temperature: 2000 K
+    constant_brightness: true
+    effects:
+      - random:
+          name: "Random Effect"
+          transition_length: 100ms
+          update_interval: 200ms
+      - strobe:
+          name: "Strobe Effect"
+      - pulse:
+          name: "Pulse Effect"
+          transition_length: 100ms
+
+  # Additional lights to test memory with multiple instances
+  - platform: rgb
+    name: "Test RGB Light"
+    id: test_rgb_light
+    red: test_red
+    green: test_green
+    blue: test_blue
+
+  - platform: binary
+    name: "Test Binary Light"
+    id: test_binary_light
+    output: test_red
diff --git a/tests/integration/fixtures/loop_disable_enable.yaml b/tests/integration/fixtures/loop_disable_enable.yaml
new file mode 100644
index 0000000000..f19d7f60ca
--- /dev/null
+++ b/tests/integration/fixtures/loop_disable_enable.yaml
@@ -0,0 +1,53 @@
+esphome:
+  name: loop-test
+
+host:
+api:
+logger:
+  level: DEBUG
+
+external_components:
+  - source:
+      type: local
+      path: EXTERNAL_COMPONENT_PATH
+
+loop_test_component:
+  components:
+    # Component that disables itself after 10 loops
+    - id: self_disable_10
+      name: "self_disable_10"
+      disable_after: 10
+
+    # Component that never disables itself (for re-enable test)
+    - id: normal_component
+      name: "normal_component"
+      disable_after: 0
+
+    # Component that tests enable when already enabled
+    - id: redundant_enable
+      name: "redundant_enable"
+      test_redundant_operations: true
+      disable_after: 0
+
+    # Component that tests disable when already disabled
+    - id: redundant_disable
+      name: "redundant_disable"
+      test_redundant_operations: true
+      disable_after: 10
+
+  # ISR test component that uses enable_loop_soon_any_context
+  isr_components:
+    - id: isr_test
+      name: "isr_test"
+
+# Interval to re-enable the self_disable_10 component after some time
+interval:
+  - interval: 0.5s
+    then:
+      - if:
+          condition:
+            lambda: 'return id(self_disable_10).get_loop_count() == 10;'
+          then:
+            - logger.log: "Re-enabling self_disable_10 via service"
+            - loop_test_component.enable:
+                id: self_disable_10
diff --git a/tests/integration/fixtures/scheduler_bulk_cleanup.yaml b/tests/integration/fixtures/scheduler_bulk_cleanup.yaml
new file mode 100644
index 0000000000..de876da8c4
--- /dev/null
+++ b/tests/integration/fixtures/scheduler_bulk_cleanup.yaml
@@ -0,0 +1,23 @@
+esphome:
+  name: scheduler-bulk-cleanup
+
+external_components:
+  - source:
+      type: local
+      path: EXTERNAL_COMPONENT_PATH
+
+host:
+
+logger:
+  level: DEBUG
+
+api:
+  services:
+    - service: trigger_bulk_cleanup
+      then:
+        - lambda: |-
+            auto component = id(bulk_cleanup_component);
+            component->trigger_bulk_cleanup();
+
+scheduler_bulk_cleanup_component:
+  id: bulk_cleanup_component
diff --git a/tests/integration/fixtures/scheduler_defer_cancel.yaml b/tests/integration/fixtures/scheduler_defer_cancel.yaml
new file mode 100644
index 0000000000..9e3f927c33
--- /dev/null
+++ b/tests/integration/fixtures/scheduler_defer_cancel.yaml
@@ -0,0 +1,51 @@
+esphome:
+  name: scheduler-defer-cancel
+
+host:
+
+logger:
+  level: DEBUG
+
+api:
+  services:
+    - service: test_defer_cancel
+      then:
+        - lambda: |-
+            // Schedule 10 defers with the same name
+            // Only the last one should execute
+            for (int i = 1; i <= 10; i++) {
+              App.scheduler.set_timeout(nullptr, "test_defer", 0, [i]() {
+                ESP_LOGI("TEST", "Defer executed: %d", i);
+                // Fire event with the defer number
+                std::string event_type = "defer_executed_" + std::to_string(i);
+                id(test_result)->trigger(event_type);
+              });
+            }
+
+            // Schedule completion notification after all defers
+            App.scheduler.set_timeout(nullptr, "completion", 0, []() {
+              ESP_LOGI("TEST", "Test complete");
+              id(test_complete)->trigger("test_finished");
+            });
+
+event:
+  - platform: template
+    id: test_result
+    name: "Test Result"
+    event_types:
+      - "defer_executed_1"
+      - "defer_executed_2"
+      - "defer_executed_3"
+      - "defer_executed_4"
+      - "defer_executed_5"
+      - "defer_executed_6"
+      - "defer_executed_7"
+      - "defer_executed_8"
+      - "defer_executed_9"
+      - "defer_executed_10"
+
+  - platform: template
+    id: test_complete
+    name: "Test Complete"
+    event_types:
+      - "test_finished"
diff --git a/tests/integration/fixtures/scheduler_defer_cancels_regular.yaml b/tests/integration/fixtures/scheduler_defer_cancels_regular.yaml
new file mode 100644
index 0000000000..fb6b1791dc
--- /dev/null
+++ b/tests/integration/fixtures/scheduler_defer_cancels_regular.yaml
@@ -0,0 +1,34 @@
+esphome:
+  name: scheduler-defer-cancel-regular
+
+host:
+
+logger:
+  level: DEBUG
+
+api:
+  services:
+    - service: test_defer_cancels_regular
+      then:
+        - lambda: |-
+            ESP_LOGI("TEST", "Starting defer cancels regular timeout test");
+
+            // Schedule a regular timeout with 100ms delay
+            App.scheduler.set_timeout(nullptr, "test_timeout", 100, []() {
+              ESP_LOGE("TEST", "ERROR: Regular timeout executed - should have been cancelled!");
+            });
+
+            ESP_LOGI("TEST", "Scheduled regular timeout with 100ms delay");
+
+            // Immediately schedule a deferred timeout (0 delay) with the same name
+            // This should cancel the regular timeout
+            App.scheduler.set_timeout(nullptr, "test_timeout", 0, []() {
+              ESP_LOGI("TEST", "SUCCESS: Deferred timeout executed");
+            });
+
+            ESP_LOGI("TEST", "Scheduled deferred timeout - should cancel regular timeout");
+
+            // Schedule test completion after 200ms (after regular timeout would have fired)
+            App.scheduler.set_timeout(nullptr, "test_complete", 200, []() {
+              ESP_LOGI("TEST", "Test complete");
+            });
diff --git a/tests/integration/fixtures/scheduler_defer_fifo_simple.yaml b/tests/integration/fixtures/scheduler_defer_fifo_simple.yaml
new file mode 100644
index 0000000000..7384082ac2
--- /dev/null
+++ b/tests/integration/fixtures/scheduler_defer_fifo_simple.yaml
@@ -0,0 +1,109 @@
+esphome:
+  name: scheduler-defer-fifo-simple
+
+host:
+
+logger:
+  level: DEBUG
+
+api:
+  services:
+    - service: test_set_timeout
+      then:
+        - lambda: |-
+            // Test set_timeout with 0 delay (direct scheduler call)
+            static int set_timeout_order = 0;
+            static bool set_timeout_passed = true;
+
+            // Reset for this test
+            set_timeout_order = 0;
+            set_timeout_passed = true;
+
+            ESP_LOGD("defer_test", "Testing set_timeout(0) for FIFO order...");
+            for (int i = 0; i < 10; i++) {
+              int expected = i;
+              App.scheduler.set_timeout((Component*)nullptr, nullptr, 0, [expected]() {
+                ESP_LOGD("defer_test", "set_timeout(0) item %d executed, order %d", expected, set_timeout_order);
+                if (set_timeout_order != expected) {
+                  ESP_LOGE("defer_test", "FIFO violation in set_timeout: expected %d but got execution order %d", expected, set_timeout_order);
+                  set_timeout_passed = false;
+                }
+                set_timeout_order++;
+
+                if (set_timeout_order == 10) {
+                  if (set_timeout_passed) {
+                    ESP_LOGI("defer_test", "✓ Test PASSED - set_timeout(0) maintains FIFO order");
+                    id(test_result)->trigger("passed");
+                  } else {
+                    ESP_LOGE("defer_test", "✗ Test FAILED - set_timeout(0) executed out of order");
+                    id(test_result)->trigger("failed");
+                  }
+                  id(test_complete)->trigger("test_finished");
+                }
+              });
+            }
+
+            ESP_LOGD("defer_test", "Deferred 10 items using set_timeout(0), waiting for execution...");
+
+    - service: test_defer
+      then:
+        - lambda: |-
+            // Test defer() method (component method)
+            static int defer_order = 0;
+            static bool defer_passed = true;
+
+            // Reset for this test
+            defer_order = 0;
+            defer_passed = true;
+
+            ESP_LOGD("defer_test", "Testing defer() for FIFO order...");
+
+            // Create a test component class that exposes defer()
+            class TestComponent : public Component {
+            public:
+              void test_defer() {
+                for (int i = 0; i < 10; i++) {
+                  int expected = i;
+                  this->defer([expected]() {
+                    ESP_LOGD("defer_test", "defer() item %d executed, order %d", expected, defer_order);
+                    if (defer_order != expected) {
+                      ESP_LOGE("defer_test", "FIFO violation in defer: expected %d but got execution order %d", expected, defer_order);
+                      defer_passed = false;
+                    }
+                    defer_order++;
+
+                    if (defer_order == 10) {
+                      if (defer_passed) {
+                        ESP_LOGI("defer_test", "✓ Test PASSED - defer() maintains FIFO order");
+                        id(test_result)->trigger("passed");
+                      } else {
+                        ESP_LOGE("defer_test", "✗ Test FAILED - defer() executed out of order");
+                        id(test_result)->trigger("failed");
+                      }
+                      id(test_complete)->trigger("test_finished");
+                    }
+                  });
+                }
+              }
+            };
+
+            // Use a static instance so it doesn't go out of scope
+            static TestComponent test_component;
+            test_component.test_defer();
+
+            ESP_LOGD("defer_test", "Deferred 10 items using defer(), waiting for execution...");
+
+event:
+  - platform: template
+    name: "Test Complete"
+    id: test_complete
+    device_class: button
+    event_types:
+      - "test_finished"
+  - platform: template
+    name: "Test Result"
+    id: test_result
+    device_class: button
+    event_types:
+      - "passed"
+      - "failed"
diff --git a/tests/integration/fixtures/scheduler_defer_stress.yaml b/tests/integration/fixtures/scheduler_defer_stress.yaml
new file mode 100644
index 0000000000..0d9c1d1405
--- /dev/null
+++ b/tests/integration/fixtures/scheduler_defer_stress.yaml
@@ -0,0 +1,38 @@
+esphome:
+  name: scheduler-defer-stress-test
+
+external_components:
+  - source:
+      type: local
+      path: EXTERNAL_COMPONENT_PATH
+    components: [defer_stress_component]
+
+host:
+
+logger:
+  level: VERBOSE
+
+defer_stress_component:
+  id: defer_stress
+
+api:
+  services:
+    - service: run_stress_test
+      then:
+        - lambda: |-
+            id(defer_stress)->run_multi_thread_test();
+
+event:
+  - platform: template
+    name: "Test Complete"
+    id: test_complete
+    device_class: button
+    event_types:
+      - "test_finished"
+  - platform: template
+    name: "Test Result"
+    id: test_result
+    device_class: button
+    event_types:
+      - "passed"
+      - "failed"
diff --git a/tests/integration/fixtures/scheduler_heap_stress.yaml b/tests/integration/fixtures/scheduler_heap_stress.yaml
new file mode 100644
index 0000000000..d4d340b68b
--- /dev/null
+++ b/tests/integration/fixtures/scheduler_heap_stress.yaml
@@ -0,0 +1,38 @@
+esphome:
+  name: scheduler-heap-stress-test
+
+external_components:
+  - source:
+      type: local
+      path: EXTERNAL_COMPONENT_PATH
+    components: [scheduler_heap_stress_component]
+
+host:
+
+logger:
+  level: VERBOSE
+
+scheduler_heap_stress_component:
+  id: heap_stress
+
+api:
+  services:
+    - service: run_heap_stress_test
+      then:
+        - lambda: |-
+            id(heap_stress)->run_multi_thread_test();
+
+event:
+  - platform: template
+    name: "Test Complete"
+    id: test_complete
+    device_class: button
+    event_types:
+      - "test_finished"
+  - platform: template
+    name: "Test Result"
+    id: test_result
+    device_class: button
+    event_types:
+      - "passed"
+      - "failed"
diff --git a/tests/integration/fixtures/scheduler_rapid_cancellation.yaml b/tests/integration/fixtures/scheduler_rapid_cancellation.yaml
new file mode 100644
index 0000000000..4824654c5c
--- /dev/null
+++ b/tests/integration/fixtures/scheduler_rapid_cancellation.yaml
@@ -0,0 +1,38 @@
+esphome:
+  name: sched-rapid-cancel-test
+
+external_components:
+  - source:
+      type: local
+      path: EXTERNAL_COMPONENT_PATH
+    components: [scheduler_rapid_cancellation_component]
+
+host:
+
+logger:
+  level: VERBOSE
+
+scheduler_rapid_cancellation_component:
+  id: rapid_cancel
+
+api:
+  services:
+    - service: run_rapid_cancellation_test
+      then:
+        - lambda: |-
+            id(rapid_cancel)->run_rapid_cancellation_test();
+
+event:
+  - platform: template
+    name: "Test Complete"
+    id: test_complete
+    device_class: button
+    event_types:
+      - "test_finished"
+  - platform: template
+    name: "Test Result"
+    id: test_result
+    device_class: button
+    event_types:
+      - "passed"
+      - "failed"
diff --git a/tests/integration/fixtures/scheduler_recursive_timeout.yaml b/tests/integration/fixtures/scheduler_recursive_timeout.yaml
new file mode 100644
index 0000000000..f1168802f6
--- /dev/null
+++ b/tests/integration/fixtures/scheduler_recursive_timeout.yaml
@@ -0,0 +1,38 @@
+esphome:
+  name: sched-recursive-timeout
+
+external_components:
+  - source:
+      type: local
+      path: EXTERNAL_COMPONENT_PATH
+    components: [scheduler_recursive_timeout_component]
+
+host:
+
+logger:
+  level: VERBOSE
+
+scheduler_recursive_timeout_component:
+  id: recursive_timeout
+
+api:
+  services:
+    - service: run_recursive_timeout_test
+      then:
+        - lambda: |-
+            id(recursive_timeout)->run_recursive_timeout_test();
+
+event:
+  - platform: template
+    name: "Test Complete"
+    id: test_complete
+    device_class: button
+    event_types:
+      - "test_finished"
+  - platform: template
+    name: "Test Result"
+    id: test_result
+    device_class: button
+    event_types:
+      - "passed"
+      - "failed"
diff --git a/tests/integration/fixtures/scheduler_simultaneous_callbacks.yaml b/tests/integration/fixtures/scheduler_simultaneous_callbacks.yaml
new file mode 100644
index 0000000000..446ee7fdc0
--- /dev/null
+++ b/tests/integration/fixtures/scheduler_simultaneous_callbacks.yaml
@@ -0,0 +1,23 @@
+esphome:
+  name: sched-simul-callbacks-test
+
+external_components:
+  - source:
+      type: local
+      path: EXTERNAL_COMPONENT_PATH
+    components: [scheduler_simultaneous_callbacks_component]
+
+host:
+
+logger:
+  level: INFO
+
+scheduler_simultaneous_callbacks_component:
+  id: simultaneous_callbacks
+
+api:
+  services:
+    - service: run_simultaneous_callbacks_test
+      then:
+        - lambda: |-
+            id(simultaneous_callbacks)->run_simultaneous_callbacks_test();
diff --git a/tests/integration/fixtures/scheduler_string_lifetime.yaml b/tests/integration/fixtures/scheduler_string_lifetime.yaml
new file mode 100644
index 0000000000..ebd5052b8b
--- /dev/null
+++ b/tests/integration/fixtures/scheduler_string_lifetime.yaml
@@ -0,0 +1,47 @@
+esphome:
+  name: scheduler-string-lifetime-test
+
+external_components:
+  - source:
+      type: local
+      path: EXTERNAL_COMPONENT_PATH
+    components: [scheduler_string_lifetime_component]
+
+host:
+
+logger:
+  level: DEBUG
+
+scheduler_string_lifetime_component:
+  id: string_lifetime
+
+api:
+  services:
+    - service: run_string_lifetime_test
+      then:
+        - lambda: |-
+            id(string_lifetime)->run_string_lifetime_test();
+    - service: run_test1
+      then:
+        - lambda: |-
+            id(string_lifetime)->run_test1();
+    - service: run_test2
+      then:
+        - lambda: |-
+            id(string_lifetime)->run_test2();
+    - service: run_test3
+      then:
+        - lambda: |-
+            id(string_lifetime)->run_test3();
+    - service: run_test4
+      then:
+        - lambda: |-
+            id(string_lifetime)->run_test4();
+    - service: run_test5
+      then:
+        - lambda: |-
+            id(string_lifetime)->run_test5();
+    - service: run_final_check
+      then:
+        - lambda: |-
+            id(string_lifetime)->run_final_check();
diff --git a/tests/integration/fixtures/scheduler_string_name_stress.yaml b/tests/integration/fixtures/scheduler_string_name_stress.yaml
new file mode 100644
index 0000000000..d1ef55c8d5
--- /dev/null
+++ b/tests/integration/fixtures/scheduler_string_name_stress.yaml
@@ -0,0 +1,38 @@
+esphome:
+  name: sched-string-name-stress
+
+external_components:
+  - source:
+      type: local
+      path: EXTERNAL_COMPONENT_PATH
+    components: [scheduler_string_name_stress_component]
+
+host:
+
+logger:
+  level: VERBOSE
+
+scheduler_string_name_stress_component:
+  id: string_stress
+
+api:
+  services:
+    - service: run_string_name_stress_test
+      then:
+        - lambda: |-
+            id(string_stress)->run_string_name_stress_test();
+
+event:
+  - platform: template
+    name: "Test Complete"
+    id: test_complete
+    device_class: button
+    event_types:
+      - "test_finished"
+  - platform: template
+    name: "Test Result"
+    id: test_result
+    device_class: button
+    event_types:
+      - "passed"
+      - "failed"
diff --git a/tests/integration/fixtures/scheduler_string_test.yaml b/tests/integration/fixtures/scheduler_string_test.yaml
new file mode 100644
index 0000000000..3dfe891370
--- /dev/null
+++ b/tests/integration/fixtures/scheduler_string_test.yaml
@@ -0,0 +1,201 @@
+esphome:
+  name: scheduler-string-test
+  on_boot:
+    priority: -100
+    then:
+      - logger.log: "Starting scheduler string tests"
+  platformio_options:
+    build_flags:
+      - "-DESPHOME_DEBUG_SCHEDULER"  # Enable scheduler debug logging
+
+host:
+api:
+logger:
+  level: VERBOSE
+
+globals:
+  - id: timeout_counter
+    type: int
+    initial_value: '0'
+  - id: interval_counter
+    type: int
+    initial_value: '0'
+  - id: dynamic_counter
+    type: int
+    initial_value: '0'
+  - id: static_tests_done
+    type: bool
+    initial_value: 'false'
+  - id: dynamic_tests_done
+    type: bool
+    initial_value: 'false'
+  - id: results_reported
+    type: bool
+    initial_value: 'false'
+
+script:
+  - id: test_static_strings
+    then:
+      - logger.log: "Testing static string timeouts and intervals"
+      - lambda: |-
+          auto *component1 = id(test_sensor1);
+          // Test 1: Static string literals with set_timeout
+          App.scheduler.set_timeout(component1, "static_timeout_1", 50, []() {
+            ESP_LOGI("test", "Static timeout 1 fired");
+            id(timeout_counter) += 1;
+          });
+
+          // Test 2: Static const char* with set_timeout
+          static const char* TIMEOUT_NAME = "static_timeout_2";
+          App.scheduler.set_timeout(component1, TIMEOUT_NAME, 100, []() {
+            ESP_LOGI("test", "Static timeout 2 fired");
+            id(timeout_counter) += 1;
+          });
+
+          // Test 3: Static string literal with set_interval
+          App.scheduler.set_interval(component1, "static_interval_1", 200, []() {
+            ESP_LOGI("test", "Static interval 1 fired, count: %d", id(interval_counter));
+            id(interval_counter) += 1;
+            if (id(interval_counter) >= 3) {
+              App.scheduler.cancel_interval(id(test_sensor1), "static_interval_1");
+              ESP_LOGI("test", "Cancelled static interval 1");
+            }
+          });
+
+          // Test 4: Empty string (should be handled safely)
+          App.scheduler.set_timeout(component1, "", 150, []() {
+            ESP_LOGI("test", "Empty string timeout fired");
+          });
+
+          // Test 5: Cancel timeout with const char* literal
+          App.scheduler.set_timeout(component1, "cancel_static_timeout", 5000, []() {
+            ESP_LOGI("test", "This static timeout should be cancelled");
+          });
+          // Cancel using const char* directly
+          App.scheduler.cancel_timeout(component1, "cancel_static_timeout");
+          ESP_LOGI("test", "Cancelled static timeout using const char*");
+
+          // Test 6 & 7: Test defer with const char* overload using a test component
+          class TestDeferComponent : public Component {
+          public:
+            void test_static_defer() {
+              // Test 6: Static string literal with defer (const char* overload)
+              this->defer("static_defer_1", []() {
+                ESP_LOGI("test", "Static defer 1 fired");
+                id(timeout_counter) += 1;
+              });
+
+              // Test 7: Static const char* with defer
+              static const char* DEFER_NAME = "static_defer_2";
+              this->defer(DEFER_NAME, []() {
+                ESP_LOGI("test", "Static defer 2 fired");
+                id(timeout_counter) += 1;
+              });
+            }
+          };
+
+          static TestDeferComponent test_defer_component;
+          test_defer_component.test_static_defer();
+
+  - id: test_dynamic_strings
+    then:
+      - logger.log: "Testing dynamic string timeouts and intervals"
+      - lambda: |-
+          auto *component2 = id(test_sensor2);
+
+          // Test 8: Dynamic string with set_timeout (std::string)
+          std::string dynamic_name = "dynamic_timeout_" + std::to_string(id(dynamic_counter)++);
+          App.scheduler.set_timeout(component2, dynamic_name, 100, []() {
+            ESP_LOGI("test", "Dynamic timeout fired");
+            id(timeout_counter) += 1;
+          });
+
+          // Test 9: Dynamic string with set_interval
+          std::string interval_name = "dynamic_interval_" + std::to_string(id(dynamic_counter)++);
+          App.scheduler.set_interval(component2, interval_name, 250, [interval_name]() {
+            ESP_LOGI("test", "Dynamic interval fired: %s", interval_name.c_str());
+            id(interval_counter) += 1;
+            if (id(interval_counter) >= 6) {
+              App.scheduler.cancel_interval(id(test_sensor2), interval_name);
+              ESP_LOGI("test", "Cancelled dynamic interval");
+            }
+          });
+
+          // Test 10: Cancel with different string object but same content
+          std::string cancel_name = "cancel_test";
+          App.scheduler.set_timeout(component2, cancel_name, 2000, []() {
+            ESP_LOGI("test", "This should be cancelled");
+          });
+
+          // Cancel using a different string object
+          std::string cancel_name_2 = "cancel_test";
+          App.scheduler.cancel_timeout(component2, cancel_name_2);
+          ESP_LOGI("test", "Cancelled timeout using different string object");
+
+          // Test 11: Dynamic string with defer (using std::string overload)
+          class TestDynamicDeferComponent : public Component {
+          public:
+            void test_dynamic_defer() {
+              std::string defer_name = "dynamic_defer_" + std::to_string(id(dynamic_counter)++);
+              this->defer(defer_name, [defer_name]() {
+                ESP_LOGI("test", "Dynamic defer fired: %s", defer_name.c_str());
+                id(timeout_counter) += 1;
+              });
+            }
+          };
+
+          static TestDynamicDeferComponent test_dynamic_defer_component;
+          test_dynamic_defer_component.test_dynamic_defer();
+
+  - id: report_results
+    then:
+      - lambda: |-
+          ESP_LOGI("test", "Final results - Timeouts: %d, Intervals: %d",
+                   id(timeout_counter), id(interval_counter));
+
+sensor:
+  - platform: template
+    name: Test Sensor 1
+    id: test_sensor1
+    lambda: return 1.0;
+    update_interval: never
+
+  - platform: template
+    name: Test Sensor 2
+    id: test_sensor2
+    lambda: return 2.0;
+    update_interval: never
+
+interval:
+  # Run static string tests after boot - using script to run once
+  - interval: 0.1s
+    then:
+      - if:
+          condition:
+            lambda: 'return id(static_tests_done) == false;'
+          then:
+            - lambda: 'id(static_tests_done) = true;'
+            - script.execute: test_static_strings
+            - logger.log: "Started static string tests"
+
+  # Run dynamic string tests after static tests
+  - interval: 0.2s
+    then:
+      - if:
+          condition:
+            lambda: 'return id(static_tests_done) && !id(dynamic_tests_done);'
+          then:
+            - lambda: 'id(dynamic_tests_done) = true;'
+            - delay: 0.2s
+            - script.execute: test_dynamic_strings
+
+  # Report results after all tests
+  - interval: 0.2s
+    then:
+      - if:
+          condition:
+            lambda: 'return id(dynamic_tests_done) && !id(results_reported);'
+          then:
+            - lambda: 'id(results_reported) = true;'
+            - delay: 1s
+            - script.execute: report_results
diff --git a/tests/integration/test_api_conditional_memory.py b/tests/integration/test_api_conditional_memory.py
new file mode 100644
index 0000000000..cfa32c431d
--- /dev/null
+++ b/tests/integration/test_api_conditional_memory.py
@@ -0,0 +1,111 @@
+"""Integration test for API conditional memory optimization with triggers and services."""
+
+from __future__ import annotations
+
+import asyncio
+import re
+
+from aioesphomeapi import UserService, UserServiceArgType
+import pytest
+
+from .types import APIClientConnectedFactory, RunCompiledFunction
+
+
+@pytest.mark.asyncio
+async def test_api_conditional_memory(
+    yaml_config: str,
+    run_compiled: RunCompiledFunction,
+    api_client_connected: APIClientConnectedFactory,
+) -> None:
+    """Test API triggers and services work correctly with conditional compilation."""
+    loop = asyncio.get_running_loop()
+
+    # Track log messages
+    connected_future = loop.create_future()
+    disconnected_future = loop.create_future()
+    service_simple_future = loop.create_future()
+    service_args_future = loop.create_future()
+
+    # Patterns to match in logs
+    connected_pattern = re.compile(r"Client .* connected from")
+    disconnected_pattern = re.compile(r"Client .* disconnected from")
+    service_simple_pattern = re.compile(r"Simple service called")
+    service_args_pattern = re.compile(
+        r"Service called with: test_string, 123, 1, 42\.50"
+    )
+
+    def check_output(line: str) -> None:
+        """Check log output for expected messages."""
+        if not connected_future.done() and connected_pattern.search(line):
+            connected_future.set_result(True)
+        elif not disconnected_future.done() and disconnected_pattern.search(line):
+            disconnected_future.set_result(True)
+        elif not service_simple_future.done() and service_simple_pattern.search(line):
+            service_simple_future.set_result(True)
+        elif not service_args_future.done() and service_args_pattern.search(line):
+            service_args_future.set_result(True)
+
+    # Run with log monitoring
+    async with run_compiled(yaml_config, line_callback=check_output):
+        async with api_client_connected() as client:
+            # Verify device info
+            device_info = await client.device_info()
+            assert device_info is not None
+            assert device_info.name == "api-conditional-memory-test"
+
+            # Wait for connection log
+            await asyncio.wait_for(connected_future, timeout=5.0)
+
+            # List services
+            _, services = await client.list_entities_services()
+
+            # Verify services exist
+            assert len(services) == 2, f"Expected 2 services, found {len(services)}"
+
+            # Find our services
+            simple_service: UserService | None = None
+            service_with_args: UserService | None = None
+
+            for service in services:
+                if service.name == "test_simple_service":
+                    simple_service = service
+                elif service.name == "test_service_with_args":
+                    service_with_args = service
+
+            assert simple_service is not None, "test_simple_service not found"
+            assert service_with_args is not None, "test_service_with_args not found"
+
+            # Verify service arguments
+            assert len(service_with_args.args) == 4, (
+                f"Expected 4 args, found {len(service_with_args.args)}"
+            )
+
+            # Check arg types
+            arg_types = {arg.name: arg.type for arg in service_with_args.args}
+            assert arg_types["arg_string"] == UserServiceArgType.STRING
+            assert arg_types["arg_int"] == UserServiceArgType.INT
+            assert arg_types["arg_bool"] == UserServiceArgType.BOOL
+            assert arg_types["arg_float"] == UserServiceArgType.FLOAT
+
+            # Call simple service
+            client.execute_service(simple_service, {})
+
+            # Wait for service log
+            await asyncio.wait_for(service_simple_future, timeout=5.0)
+
+            # Call service with arguments
+            client.execute_service(
+                service_with_args,
+                {
+                    "arg_string": "test_string",
+                    "arg_int": 123,
+                    "arg_bool": True,
+                    "arg_float": 42.5,
+                },
+            )
+
+            # Wait for service with args log
+            await asyncio.wait_for(service_args_future, timeout=5.0)
+
+        # Client disconnected here, wait for disconnect log
+        await asyncio.wait_for(disconnected_future, timeout=5.0)
diff --git a/tests/integration/test_api_reboot_timeout.py b/tests/integration/test_api_reboot_timeout.py
new file mode 100644
index 0000000000..dd9f5fbd1e
--- /dev/null
+++ b/tests/integration/test_api_reboot_timeout.py
@@ -0,0 +1,35 @@
+"""Test API server reboot timeout functionality."""
+
+import asyncio
+import re
+
+import pytest
+
+from .types import RunCompiledFunction
+
+
+@pytest.mark.asyncio
+async def test_api_reboot_timeout(
+    yaml_config: str,
+    run_compiled: RunCompiledFunction,
+) -> None:
+    """Test that the device reboots when no API clients connect within the timeout."""
+    loop = asyncio.get_running_loop()
+    reboot_future = loop.create_future()
+    reboot_pattern = re.compile(r"No clients; rebooting")
+
+    def check_output(line: str) -> None:
+        """Check output for reboot message."""
+        if not reboot_future.done() and reboot_pattern.search(line):
+            reboot_future.set_result(True)
+
+    # Run the device without connecting any API client
+    async with run_compiled(yaml_config, line_callback=check_output):
+        # Wait for reboot with timeout
+        # (0.5s reboot timeout + some margin for processing)
+        try:
+            await asyncio.wait_for(reboot_future, timeout=2.0)
+        except asyncio.TimeoutError:
+            pytest.fail("Device did not reboot within expected timeout")
+
+    # Test passes if we get here - reboot was detected
diff --git a/tests/integration/test_api_vv_logging.py b/tests/integration/test_api_vv_logging.py
new file mode 100644
index 0000000000..fcbdd341ae
--- /dev/null
+++ b/tests/integration/test_api_vv_logging.py
@@ -0,0 +1,83 @@
+"""Integration test for API with VERY_VERBOSE logging to verify no buffer corruption."""
+
+from __future__ import annotations
+
+import asyncio
+from typing import Any
+
+from aioesphomeapi import LogLevel, SensorInfo
+import pytest
+
+from .types import APIClientConnectedFactory, RunCompiledFunction
+
+
+@pytest.mark.asyncio
+async def test_api_vv_logging(
+    yaml_config: str,
+    run_compiled: RunCompiledFunction,
+    api_client_connected: APIClientConnectedFactory,
+) -> None:
+    """Test that VERY_VERBOSE logging doesn't cause buffer corruption with API messages."""
+
+    # Track that we're receiving VV log messages and sensor updates
+    vv_logs_received = 0
+    sensor_updates_received = 0
+    errors_detected = []
+
+    def on_log(msg: Any) -> None:
+        """Capture log messages."""
+        nonlocal vv_logs_received
+        # msg is a SubscribeLogsResponse object with 'message' attribute
+        # The message field is always bytes
+        message_text = msg.message.decode("utf-8", errors="replace")
+
+        # Only count VV logs specifically
+        if "[VV]" in message_text:
+            vv_logs_received += 1
+
+        # Check for assertion or error messages
+        if "assert" in message_text.lower() or "error" in message_text.lower():
+            errors_detected.append(message_text)
+
+    # Write, compile and run the ESPHome device
+    async with run_compiled(yaml_config), api_client_connected() as client:
+        # Subscribe to VERY_VERBOSE logs - this enables the code path that could cause corruption
+        client.subscribe_logs(on_log, log_level=LogLevel.LOG_LEVEL_VERY_VERBOSE)
+
+        # Wait for device to be ready
+        device_info = await client.device_info()
+        assert device_info is not None
+        assert device_info.name == "vv-logging-test"
+
+        # Subscribe to sensor states
+        states = {}
+
+        def on_state(state):
+            nonlocal sensor_updates_received
+            sensor_updates_received += 1
+            states[state.key] = state
+
+        client.subscribe_states(on_state)
+
+        # List entities to find our test sensors
+        entity_info, _ = await client.list_entities_services()
+
+        # Count sensors
+        sensor_count = sum(1 for e in entity_info if isinstance(e, SensorInfo))
+        assert sensor_count >= 10, f"Expected at least 10 sensors, got {sensor_count}"
+
+        # Wait for sensor updates to flow with VV logging active
+        # The sensors update every 50ms, so we should get many updates
+        await asyncio.sleep(0.25)
+
+        # Verify we received both VV logs and sensor updates
+        assert vv_logs_received > 0, "Expected to receive VERY_VERBOSE log messages"
+        assert sensor_updates_received > 10, (
+            f"Expected many sensor updates, got {sensor_updates_received}"
+        )
+
+        # Check for any errors
+        if errors_detected:
+            pytest.fail(f"Errors detected during test: {errors_detected}")
+
+        # The test passes if we didn't hit any assertions or buffer corruption
diff --git a/tests/integration/test_areas_and_devices.py b/tests/integration/test_areas_and_devices.py
new file mode 100644
index 0000000000..4184255724
--- /dev/null
+++ b/tests/integration/test_areas_and_devices.py
@@ -0,0 +1,121 @@
+"""Integration test for areas and devices feature."""
+
+from __future__ import annotations
+
+import asyncio
+
+from aioesphomeapi import EntityState
+import pytest
+
+from .types import APIClientConnectedFactory, RunCompiledFunction
+
+
+@pytest.mark.asyncio
+async def test_areas_and_devices(
+    yaml_config: str,
+    run_compiled: RunCompiledFunction,
+    api_client_connected: APIClientConnectedFactory,
+) -> None:
+    """Test areas and devices configuration with entity mapping."""
+    async with run_compiled(yaml_config), api_client_connected() as client:
+        # Get device info which includes areas and devices
+        device_info = await client.device_info()
+        assert device_info is not None
+
+        # Verify areas are reported
+        areas = device_info.areas
+        assert len(areas) >= 2, f"Expected at least 2 areas, got {len(areas)}"
+
+        # Find our specific areas
+        main_area = next((a for a in areas if a.name == "Living Room"), None)
+        bedroom_area = next((a for a in areas if a.name == "Bedroom"), None)
+        kitchen_area = next((a for a in areas if a.name == "Kitchen"), None)
+
+        assert main_area is not None, "Living Room area not found"
+        assert bedroom_area is not None, "Bedroom area not found"
+        assert kitchen_area is not None, "Kitchen area not found"
+
+        # Verify devices are reported
+        devices = device_info.devices
+        assert len(devices) >= 4, f"Expected at least 4 devices, got {len(devices)}"
+
+        # Find our specific devices
+        light_controller = next(
+            (d for d in devices if d.name == "Light Controller"), None
+        )
+        temp_sensor = next((d for d in devices if d.name == "Temperature Sensor"), None)
+        motion_detector = next(
+            (d for d in devices if d.name == "Motion Detector"), None
+        )
+        smart_switch = next((d for d in devices if d.name == "Smart Switch"), None)
+
+        assert light_controller is not None, "Light Controller device not found"
+        assert temp_sensor is not None, "Temperature Sensor device not found"
+        assert motion_detector is not None, "Motion Detector device not found"
+        assert smart_switch is not None, "Smart Switch device not found"
+
+        # Verify device area assignments
+        assert light_controller.area_id == main_area.area_id, (
+            "Light Controller should be in Living Room"
+        )
+        assert temp_sensor.area_id == bedroom_area.area_id, (
+            "Temperature Sensor should be in Bedroom"
+        )
+        assert motion_detector.area_id == main_area.area_id, (
+            "Motion Detector should be in Living Room"
+        )
+        assert smart_switch.area_id == kitchen_area.area_id, (
+            "Smart Switch should be in Kitchen"
+        )
+
+        # Verify suggested_area is set to the top-level area name
+        assert device_info.suggested_area == "Living Room", (
+            f"Expected suggested_area to be 'Living Room', got '{device_info.suggested_area}'"
+        )
+
+        # Get entity list to verify device_id mapping
+        entities = await client.list_entities_services()
+
+        # Collect sensor entities (all entities have device_id)
+        sensor_entities = entities[0]
+        assert len(sensor_entities) >= 4, (
+            f"Expected at least 4 sensor entities, got {len(sensor_entities)}"
+        )
+
+        # Subscribe to states to get sensor values
+        loop = asyncio.get_running_loop()
+        states: dict[int, EntityState] = {}
+        states_future: asyncio.Future[bool] = loop.create_future()
+
+        def on_state(state: EntityState) -> None:
+            states[state.key] = state
+            # Check if we have all expected sensor states
+            if len(states) >= 4 and not states_future.done():
+                states_future.set_result(True)
+
+        client.subscribe_states(on_state)
+
+        # Wait for sensor states
+        try:
+            await asyncio.wait_for(states_future, timeout=10.0)
+        except asyncio.TimeoutError:
+            pytest.fail(
+                f"Did not receive all sensor states within 10 seconds. "
+                f"Received {len(states)} states"
+            )
+
+        # Verify we have sensor entities with proper device_id assignments
+        device_id_mapping = {
+            "Light Controller Sensor": light_controller.device_id,
+            "Temperature Sensor Reading": temp_sensor.device_id,
+            "Motion Detector Status": motion_detector.device_id,
+            "Smart Switch Power": smart_switch.device_id,
+        }
+
+        for entity in sensor_entities:
+            if entity.name in device_id_mapping:
+                expected_device_id = device_id_mapping[entity.name]
+                assert entity.device_id == expected_device_id, (
+                    f"{entity.name} has device_id {entity.device_id}, "
+                    f"expected {expected_device_id}"
+                )
diff --git a/tests/integration/test_batch_delay_zero_rapid_transitions.py b/tests/integration/test_batch_delay_zero_rapid_transitions.py
new file mode 100644
index 0000000000..f17319dddf
--- /dev/null
+++ b/tests/integration/test_batch_delay_zero_rapid_transitions.py
@@ -0,0 +1,58 @@
+"""Integration test for API batch_delay: 0 with rapid state transitions."""
+
+from __future__ import annotations
+
+import asyncio
+import time
+
+from aioesphomeapi import BinarySensorInfo, BinarySensorState, EntityState
+import pytest
+
+from .types import APIClientConnectedFactory, RunCompiledFunction
+
+
+@pytest.mark.asyncio
+async def test_batch_delay_zero_rapid_transitions(
+    yaml_config: str,
+    run_compiled: RunCompiledFunction,
+    api_client_connected: APIClientConnectedFactory,
+) -> None:
+    """Test that rapid binary sensor transitions are preserved with batch_delay: 0ms."""
+    async with run_compiled(yaml_config), api_client_connected() as client:
+        # Track state changes
+        state_changes: list[tuple[bool, float]] = []
+
+        def on_state(state: EntityState) -> None:
+            """Track state changes with timestamps."""
+            if isinstance(state, BinarySensorState):
+                state_changes.append((state.state, time.monotonic()))
+
+        # Subscribe to state changes
+        client.subscribe_states(on_state)
+
+        # Wait for entity info
+        entity_info, _ = await client.list_entities_services()
+        binary_sensors = [e for e in entity_info if isinstance(e, BinarySensorInfo)]
+        assert len(binary_sensors) == 1, "Expected 1 binary sensor"
+
+        # Collect states for 2 seconds
+        await asyncio.sleep(2.1)
+
+        # Count ON->OFF transitions
+        on_off_count = 0
+        for i in range(1, len(state_changes)):
+            if state_changes[i - 1][0] and not state_changes[i][0]:  # ON to OFF
+                on_off_count += 1
+
+        # With batch_delay: 0, we should capture rapid transitions
+        # The test timing can be variable in CI, so we're being conservative
+        # We mainly want to verify that we capture multiple rapid transitions
+        assert on_off_count >= 5, (
+            f"Expected at least 5 ON->OFF transitions with batch_delay: 0ms, got {on_off_count}. "
+            "Rapid transitions may have been lost."
+        )
+
+        # Also verify that state changes are happening frequently
+        assert len(state_changes) >= 10, (
+            f"Expected at least 10 state changes, got {len(state_changes)}"
+        )
diff --git a/tests/integration/test_device_id_in_state.py b/tests/integration/test_device_id_in_state.py
new file mode 100644
index 0000000000..eaa91ec92e
--- /dev/null
+++ b/tests/integration/test_device_id_in_state.py
@@ -0,0 +1,147 @@
+"""Integration test for device_id in entity state responses."""
+
+from __future__ import annotations
+
+import asyncio
+
+from aioesphomeapi import BinarySensorState, EntityState, SensorState, TextSensorState
+import pytest
+
+from .types import APIClientConnectedFactory, RunCompiledFunction
+
+
+@pytest.mark.asyncio
+async def test_device_id_in_state(
+    yaml_config: str,
+    run_compiled: RunCompiledFunction,
+    api_client_connected: APIClientConnectedFactory,
+) -> None:
+    """Test that device_id is included in entity state responses."""
+    async with run_compiled(yaml_config), api_client_connected() as client:
+        # Get device info to verify devices are configured
+        device_info = await client.device_info()
+        assert device_info is not None
+
+        # Verify devices exist
+        devices = device_info.devices
+        assert len(devices) >= 3, f"Expected at least 3 devices, got {len(devices)}"
+
+        # Get device IDs for verification
+        device_ids = {device.name: device.device_id for device in devices}
+        assert "Temperature Monitor" in device_ids
+        assert "Humidity Monitor" in device_ids
+        assert "Motion Sensor" in device_ids
+
+        # Get entity list
+        entities = await client.list_entities_services()
+        all_entities = entities[0]
+
+        # Create a mapping of entity key to expected device_id
+        entity_device_mapping: dict[int, int] = {}
+
+        for entity in all_entities:
+            # All entities have name and key attributes
+            if entity.name == "Temperature":
+                entity_device_mapping[entity.key] = device_ids["Temperature Monitor"]
+            elif entity.name == "Humidity":
+                entity_device_mapping[entity.key] = device_ids["Humidity Monitor"]
+            elif entity.name == "Motion Detected":
+                entity_device_mapping[entity.key] = device_ids["Motion Sensor"]
+            elif entity.name == "Temperature Monitor Power":
+                entity_device_mapping[entity.key] = device_ids["Temperature Monitor"]
+            elif entity.name == "Temperature Status":
+                entity_device_mapping[entity.key] = device_ids["Temperature Monitor"]
+            elif entity.name == "Motion Light":
+                entity_device_mapping[entity.key] = device_ids["Motion Sensor"]
+            elif entity.name == "No Device Sensor":
+                # Entity without device_id should have device_id 0
+                entity_device_mapping[entity.key] = 0
+
+        assert len(entity_device_mapping) >= 6, (
+            f"Expected at least 6 mapped entities, got {len(entity_device_mapping)}"
+        )
+
+        # Subscribe to states
+        loop = asyncio.get_running_loop()
+        states: dict[int, EntityState] = {}
+        states_future: asyncio.Future[bool] = loop.create_future()
+
+        def on_state(state: EntityState) -> None:
+            states[state.key] = state
+            # Check if we have states for all mapped entities
+            if len(states) >= len(entity_device_mapping) and not states_future.done():
+                states_future.set_result(True)
+
+        client.subscribe_states(on_state)
+
+        # Wait for states
+        try:
+            await asyncio.wait_for(states_future, timeout=10.0)
+        except asyncio.TimeoutError:
+            pytest.fail(
+                f"Did not receive all entity states within 10 seconds. "
+                f"Received {len(states)} states, expected {len(entity_device_mapping)}"
+            )
+
+        # Verify each state has the correct device_id
+        verified_count = 0
+        for key, expected_device_id in entity_device_mapping.items():
+            if key in states:
+                state = states[key]
+
+                assert state.device_id == expected_device_id, (
+                    f"State for key {key} has device_id {state.device_id}, "
+                    f"expected {expected_device_id}"
+                )
+                verified_count += 1
+
+        assert verified_count >= 6, (
+            f"Only verified {verified_count} states, expected at least 6"
+        )
+
+        # Test specific state types to ensure device_id is present
+        # Find a sensor state with device_id
+        sensor_state = next(
+            (
+                s
+                for s in states.values()
+                if isinstance(s, SensorState)
+                and isinstance(s.state, float)
+                and s.device_id != 0
+            ),
+            None,
+        )
+        assert sensor_state is not None, "No sensor state with device_id found"
+        assert sensor_state.device_id > 0, "Sensor state should have non-zero device_id"
+
+        # Find a binary sensor state
+        binary_sensor_state = next(
+            (s for s in states.values() if isinstance(s, BinarySensorState)),
+            None,
+        )
+        assert binary_sensor_state is not None, "No binary sensor state found"
+        assert binary_sensor_state.device_id > 0, (
+            "Binary sensor state should have non-zero device_id"
+        )
+
+        # Find a text sensor state
+        text_sensor_state = next(
+            (s for s in states.values() if isinstance(s, TextSensorState)),
+            None,
+        )
+        assert text_sensor_state is not None, "No text sensor state found"
+        assert text_sensor_state.device_id > 0, (
+            "Text sensor state should have non-zero device_id"
+        )
+
+        # Verify the "No Device Sensor" has device_id = 0
+        no_device_key = next(
+            (key for key, device_id in entity_device_mapping.items() if device_id == 0),
+            None,
+        )
+        assert no_device_key is not None, "No entity mapped to device_id 0"
+        assert no_device_key in states, f"State for key {no_device_key} not found"
+        no_device_state = states[no_device_key]
+        assert no_device_state.device_id == 0, (
+            f"Entity without device_id should have device_id=0, got {no_device_state.device_id}"
+        )
diff --git a/tests/integration/test_duplicate_entities.py b/tests/integration/test_duplicate_entities.py
new file mode 100644
index 0000000000..b7ee8dd478
--- /dev/null
+++ b/tests/integration/test_duplicate_entities.py
@@ -0,0 +1,213 @@
+"""Integration test for duplicate entity handling with new validation."""
+
+from __future__ import annotations
+
+import asyncio
+
+from aioesphomeapi import EntityInfo
+import pytest
+
+from .types import APIClientConnectedFactory, RunCompiledFunction
+
+
+@pytest.mark.asyncio
+async def test_duplicate_entities_not_allowed_on_different_devices(
+    yaml_config: str,
+    run_compiled: RunCompiledFunction,
+    api_client_connected: APIClientConnectedFactory,
+) -> None:
+    """Test that duplicate entity names are NOT allowed on different devices."""
+    async with run_compiled(yaml_config), api_client_connected() as client:
+        # Get device info
+        device_info = await client.device_info()
+        assert device_info is not None
+
+        # Get devices
+        devices = device_info.devices
+        assert len(devices) >= 3, f"Expected at least 3 devices, got {len(devices)}"
+
+        # Find our test devices
+        controller_1 = next((d for d in devices if d.name == "Controller 1"), None)
+        controller_2 = next((d for d in devices if d.name == "Controller 2"), None)
+        controller_3 = next((d for d in devices if d.name == "Controller 3"), None)
+
+        assert controller_1 is not None, "Controller 1 device not found"
+        assert controller_2 is not None, "Controller 2 device not found"
+        assert controller_3 is not None, "Controller 3 device not found"
+
+        # Get entity list
+        entities = await client.list_entities_services()
+        all_entities: list[EntityInfo] = []
+        for entity_list in entities[0]:
+            all_entities.append(entity_list)
+
+        # Group entities by type for easier testing
+        sensors = [e for e in all_entities if e.__class__.__name__ == "SensorInfo"]
+        binary_sensors = [
+            e for e in all_entities if e.__class__.__name__ == "BinarySensorInfo"
+        ]
+        text_sensors = [
+            e for e in all_entities if e.__class__.__name__ == "TextSensorInfo"
+        ]
+        switches = [e for e in all_entities if e.__class__.__name__ == "SwitchInfo"]
+        buttons = [e for e in all_entities if e.__class__.__name__ == "ButtonInfo"]
+        numbers = [e for e in all_entities if e.__class__.__name__ == "NumberInfo"]
+        selects = [e for e in all_entities if e.__class__.__name__ == "SelectInfo"]
+
+        # Scenario 1: Check that temperature sensors have unique names per device
+        temp_sensors = [s for s in sensors if "Temperature" in s.name]
+        assert len(temp_sensors) == 4, (
+            f"Expected exactly 4 temperature sensors, got {len(temp_sensors)}"
+        )
+
+        # Verify each sensor has a unique name
+        temp_names = set()
+        temp_object_ids = set()
+
+        for sensor in temp_sensors:
+            temp_names.add(sensor.name)
+            temp_object_ids.add(sensor.object_id)
+
+        # Should have 4 unique names
+        assert len(temp_names) == 4, (
+            f"Temperature sensors should have unique names, got {temp_names}"
+        )
+
+        # Object IDs should also be unique
+        assert len(temp_object_ids) == 4, (
+            f"Temperature sensors should have unique object_ids, got {temp_object_ids}"
+        )
+
+        # Scenario 2: Check binary sensors have unique names
+        status_binary = [b for b in binary_sensors if "Status" in b.name]
+        assert len(status_binary) == 3, (
+            f"Expected exactly 3 status binary sensors, got {len(status_binary)}"
+        )
+
+        # All should have unique object_ids
+        status_names = set()
+        for binary in status_binary:
+            status_names.add(binary.name)
+
+        assert len(status_names) == 3, (
+            f"Status binary sensors should have unique names, got {status_names}"
+        )
+
+        # Scenario 3: Check that sensor and binary_sensor can have same name
+        temp_binary = [b for b in binary_sensors if b.name == "Temperature"]
+        assert len(temp_binary) == 1, (
+            f"Expected exactly 1 temperature binary sensor, got {len(temp_binary)}"
+        )
+        assert temp_binary[0].object_id == "temperature"
+
+        # Scenario 4: Check text sensors have unique names
+        info_text = [t for t in text_sensors if "Device Info" in t.name]
+        assert len(info_text) == 3, (
+            f"Expected exactly 3 device info text sensors, got {len(info_text)}"
+        )
+
+        # All should have unique names and object_ids
+        info_names = set()
+        for text in info_text:
+            info_names.add(text.name)
+
+        assert len(info_names) == 3, (
+            f"Device info text sensors should have unique names, got {info_names}"
+        )
+
+        # Scenario 5: Check switches have unique names
+        power_switches = [s for s in switches if "Power" in s.name]
+        assert len(power_switches) == 4, (
+            f"Expected exactly 4 power switches, got {len(power_switches)}"
+        )
+
+        # All should have unique names
+        power_names = set()
+        for switch in power_switches:
+            power_names.add(switch.name)
+
+        assert len(power_names) == 4, (
+            f"Power switches should have unique names, got {power_names}"
+        )
+
+        # Scenario 6: Check reset buttons have unique names
+        reset_buttons = [b for b in buttons if "Reset" in b.name]
+        assert len(reset_buttons) == 3, (
+            f"Expected exactly 3 reset buttons, got {len(reset_buttons)}"
+        )
+
+        # All should have unique names
+        reset_names = set()
+        for button in reset_buttons:
+            reset_names.add(button.name)
+
+        assert len(reset_names) == 3, (
+            f"Reset buttons should have unique names, got {reset_names}"
+        )
+
+        # Scenario 7: Check empty name selects (should use device names)
+        empty_selects = [s for s in selects if s.name == ""]
+        assert len(empty_selects) == 3, (
+            f"Expected exactly 3 empty name selects, got {len(empty_selects)}"
+        )
+
+        # Group by device
+        c1_selects = [s for s in empty_selects if s.device_id == controller_1.device_id]
+        c2_selects = [s for s in empty_selects if s.device_id == controller_2.device_id]
+
+        # For main device, device_id is 0
+        main_selects = [s for s in empty_selects if s.device_id == 0]
+
+        # Check object IDs for empty name entities - they should use device names
+        assert len(c1_selects) == 1 and c1_selects[0].object_id == "controller_1"
+        assert len(c2_selects) == 1 and c2_selects[0].object_id == "controller_2"
+        assert (
+            len(main_selects) == 1
+            and main_selects[0].object_id == "duplicate-entities-test"
+        )
+
+        # Scenario 8: Check special characters in number names - now unique
+        temp_numbers = [n for n in numbers if "Temperature Setpoint!" in n.name]
+        assert len(temp_numbers) == 2, (
+            f"Expected exactly 2 temperature setpoint numbers, got {len(temp_numbers)}"
+        )
+
+        # Should have unique names
+        setpoint_names = set()
+        for number in temp_numbers:
+            setpoint_names.add(number.name)
+
+        assert len(setpoint_names) == 2, (
+            f"Temperature setpoint numbers should have unique names, got {setpoint_names}"
+        )
+
+        # Verify we can get states for all entities (ensures they're functional)
+        loop = asyncio.get_running_loop()
+        states_future: asyncio.Future[None] = loop.create_future()
+        state_count = 0
+        expected_count = (
+            len(sensors)
+            + len(binary_sensors)
+            + len(text_sensors)
+            + len(switches)
+            + len(buttons)
+            + len(numbers)
+            + len(selects)
+        )
+
+        def on_state(state) -> None:
+            nonlocal state_count
+            state_count += 1
+            if state_count >= expected_count and not states_future.done():
+                states_future.set_result(None)
+
+        client.subscribe_states(on_state)
+
+        # Wait for all entity states
+        try:
+            await asyncio.wait_for(states_future, timeout=10.0)
+        except asyncio.TimeoutError:
+            pytest.fail(
+                f"Did not receive all entity states within 10 seconds. "
+                f"Expected {expected_count}, received {state_count}"
+            )
diff --git a/tests/integration/test_entity_icon.py b/tests/integration/test_entity_icon.py
new file mode 100644
index 0000000000..aec7168165
--- /dev/null
+++ b/tests/integration/test_entity_icon.py
@@ -0,0 +1,91 @@
+"""Integration test for entity icons with USE_ENTITY_ICON feature."""
+
+from __future__ import annotations
+
+import asyncio
+
+from aioesphomeapi import EntityState
+import pytest
+
+from .types import APIClientConnectedFactory, RunCompiledFunction
+
+
+@pytest.mark.asyncio
+async def test_entity_icon(
+    yaml_config: str,
+    run_compiled: RunCompiledFunction,
+    api_client_connected: APIClientConnectedFactory,
+) -> None:
+    """Test that entities with custom icons work correctly with USE_ENTITY_ICON."""
+    # Write, compile and run the ESPHome device, then connect to API
+    async with run_compiled(yaml_config), api_client_connected() as client:
+        # Get all entities
+        entities = await client.list_entities_services()
+
+        # Create a map of entity names to entity info
+        entity_map = {entity.name: entity for entity in entities[0]}
+
+        # Test entities with icons
+        icon_test_cases = [
+            # (entity_name, expected_icon)
+            ("Sensor With Icon", "mdi:temperature-celsius"),
+            ("Binary Sensor With Icon", "mdi:motion-sensor"),
+            ("Text Sensor With Icon", "mdi:text-box"),
+            ("Switch With Icon", "mdi:toggle-switch"),
+            ("Button With Icon", "mdi:gesture-tap-button"),
+            ("Number With Icon", "mdi:numeric"),
+            ("Select With Icon", "mdi:format-list-bulleted"),
+        ]
+
+        # Test entities without icons (should have empty string)
+        no_icon_test_cases = [
+            "Sensor Without Icon",
+            "Binary Sensor Without Icon",
+        ]
+
+        # Verify entities with icons
+        for entity_name, expected_icon in icon_test_cases:
+            assert entity_name in entity_map, (
+                f"Entity '{entity_name}' not found in API response"
+            )
+            entity = entity_map[entity_name]
+
+            # Check icon field
+            assert entity.icon == expected_icon, (
+                f"{entity_name}: icon mismatch - "
+                f"expected '{expected_icon}', got '{entity.icon}'"
+            )
+
+        # Verify entities without icons
+        for entity_name in no_icon_test_cases:
+            assert entity_name in entity_map, (
+                f"Entity '{entity_name}' not found in API response"
+            )
+            entity = entity_map[entity_name]
+
+            # Check icon field is empty
+            assert entity.icon == "", (
+                f"{entity_name}: icon should be empty string for entities without icons, "
+                f"got '{entity.icon}'"
+            )
+
+        # Subscribe to states to ensure everything works normally
+        states: dict[int, EntityState] = {}
+        state_received = asyncio.Event()
+
+        def on_state(state: EntityState) -> None:
+            states[state.key] = state
+            state_received.set()
+
+        client.subscribe_states(on_state)
+
+        # Wait for states
+        try:
+            await asyncio.wait_for(state_received.wait(), timeout=5.0)
+        except asyncio.TimeoutError:
+            pytest.fail("No states received within 5 seconds")
+
+        # Verify we received states
+        assert len(states) > 0, (
+            "No states received - entities may not be working correctly"
+        )
diff --git a/tests/integration/test_host_mode_empty_string_options.py b/tests/integration/test_host_mode_empty_string_options.py
index d2df839a75..16399dcfb8 100644
--- a/tests/integration/test_host_mode_empty_string_options.py
+++ b/tests/integration/test_host_mode_empty_string_options.py
@@ -74,37 +74,41 @@ async def test_host_mode_empty_string_options(
         # If we got here without protobuf decoding errors, the fix is working
         # The bug would have caused "Invalid protobuf message" errors with trailing bytes
 
-        # Also verify we can interact with the select entities
-        # Subscribe to state changes
+        # Also verify we can receive state updates for select entities
+        # This ensures empty strings work properly in state messages too
         states: dict[int, EntityState] = {}
-        state_change_future: asyncio.Future[None] = loop.create_future()
+        states_received_future: asyncio.Future[None] = loop.create_future()
+        expected_select_keys = {empty_first.key, empty_middle.key, empty_last.key}
+        received_select_keys = set()
 
         def on_state(state: EntityState) -> None:
             """Track state changes."""
             states[state.key] = state
-            # When we receive the state change for our select, resolve the future
-            if state.key == empty_first.key and not state_change_future.done():
-                state_change_future.set_result(None)
+            # Track which select entities we've received states for
+            if state.key in expected_select_keys:
+                received_select_keys.add(state.key)
+                # Once we have all select states, we're done
+                if (
+                    received_select_keys == expected_select_keys
+                    and not states_received_future.done()
+                ):
+                    states_received_future.set_result(None)
 
         client.subscribe_states(on_state)
 
-        # Try setting a select to an empty string option
-        # This further tests that empty strings are handled correctly
-        client.select_command(empty_first.key, "")
-
-        # Wait for state update with timeout
+        # Wait for initial states with timeout
         try:
-            await asyncio.wait_for(state_change_future, timeout=5.0)
+            await asyncio.wait_for(states_received_future, timeout=5.0)
         except asyncio.TimeoutError:
             pytest.fail(
-                "Did not receive state update after setting select to empty string"
+                f"Did not receive states for all select entities. "
+                f"Expected keys: {expected_select_keys}, Received: {received_select_keys}"
             )
 
-        # Verify the state was set to empty string
+        # Verify we received states for all select entities
         assert empty_first.key in states
-        select_state = states[empty_first.key]
-        assert hasattr(select_state, "state")
-        assert select_state.state == ""
+        assert empty_middle.key in states
+        assert empty_last.key in states
 
-        # The test passes if no protobuf decoding errors occurred
-        # With the bug, we would have gotten "Invalid protobuf message" errors
+        # The main test is that we got here without protobuf errors
+        # The select entities with empty string options were properly encoded
diff --git a/tests/integration/test_host_mode_entity_fields.py b/tests/integration/test_host_mode_entity_fields.py
index cf3fa6916a..b9fa3e9746 100644
--- a/tests/integration/test_host_mode_entity_fields.py
+++ b/tests/integration/test_host_mode_entity_fields.py
@@ -25,8 +25,8 @@ async def test_host_mode_entity_fields(
         # Create a map of entity names to entity info
         entity_map = {}
         for entity in entities[0]:
-            if hasattr(entity, "name"):
-                entity_map[entity.name] = entity
+            # All entities should have a name attribute
+            entity_map[entity.name] = entity
 
         # Test entities that should be visible via API (non-internal)
         visible_test_cases = [
diff --git a/tests/integration/test_host_mode_fan_preset.py b/tests/integration/test_host_mode_fan_preset.py
index 1d956a7290..d18b9f08ad 100644
--- a/tests/integration/test_host_mode_fan_preset.py
+++ b/tests/integration/test_host_mode_fan_preset.py
@@ -46,14 +46,22 @@ async def test_host_mode_fan_preset(
         # Subscribe to states
         states: dict[int, FanState] = {}
         state_event = asyncio.Event()
+        initial_states_received = set()
 
         def on_state(state: FanState) -> None:
             if isinstance(state, FanState):
                 states[state.key] = state
+                initial_states_received.add(state.key)
                 state_event.set()
 
         client.subscribe_states(on_state)
 
+        # Wait for initial states to be received for all fans
+        expected_fan_keys = {fan.key for fan in fans}
+        while initial_states_received != expected_fan_keys:
+            state_event.clear()
+            await asyncio.wait_for(state_event.wait(), timeout=2.0)
+
         # Test 1: Turn on fan without speed or preset - should set speed to 100%
         state_event.clear()
         client.fan_command(
diff --git a/tests/integration/test_host_mode_many_entities.py b/tests/integration/test_host_mode_many_entities.py
index d5622e6fa4..19d1ee315f 100644
--- a/tests/integration/test_host_mode_many_entities.py
+++ b/tests/integration/test_host_mode_many_entities.py
@@ -4,7 +4,7 @@ from __future__ import annotations
 
 import asyncio
 
-from aioesphomeapi import EntityState
+from aioesphomeapi import EntityState, SensorState
 import pytest
 
 from .types import APIClientConnectedFactory, RunCompiledFunction
@@ -22,36 +22,51 @@ async def test_host_mode_many_entities(
     async with run_compiled(yaml_config), api_client_connected() as client:
         # Subscribe to state changes
         states: dict[int, EntityState] = {}
-        entity_count_future: asyncio.Future[int] = loop.create_future()
+        sensor_count_future: asyncio.Future[int] = loop.create_future()
 
         def on_state(state: EntityState) -> None:
             states[state.key] = state
-            # When we have received states from a good number of entities, resolve the future
-            if len(states) >= 50 and not entity_count_future.done():
-                entity_count_future.set_result(len(states))
+            # Count sensor states specifically
+            sensor_states = [
+                s
+                for s in states.values()
+                if isinstance(s, SensorState) and isinstance(s.state, float)
+            ]
+            # When we have received states from at least 50 sensors, resolve the future
+            if len(sensor_states) >= 50 and not sensor_count_future.done():
+                sensor_count_future.set_result(len(sensor_states))
 
         client.subscribe_states(on_state)
 
-        # Wait for states from at least 50 entities with timeout
+        # Wait for states from at least 50 sensors with timeout
         try:
-            entity_count = await asyncio.wait_for(entity_count_future, timeout=10.0)
+            sensor_count = await asyncio.wait_for(sensor_count_future, timeout=10.0)
         except asyncio.TimeoutError:
+            sensor_states = [
+                s
+                for s in states.values()
+                if isinstance(s, SensorState) and isinstance(s.state, float)
+            ]
             pytest.fail(
-                f"Did not receive states from at least 50 entities within 10 seconds. "
-                f"Received {len(states)} states: {list(states.keys())}"
+                f"Did not receive states from at least 50 sensors within 10 seconds. "
+                f"Received {len(sensor_states)} sensor states out of {len(states)} total states"
             )
 
         # Verify we received a good number of entity states
-        assert entity_count >= 50, f"Expected at least 50 entities, got {entity_count}"
-        assert len(states) >= 50, f"Expected at least 50 states, got {len(states)}"
+        assert len(states) >= 50, (
+            f"Expected at least 50 total states, got {len(states)}"
+        )
 
-        # Verify we have different entity types by checking some expected values
+        # Verify we have the expected sensor states
         sensor_states = [
             s
             for s in states.values()
-            if hasattr(s, "state") and isinstance(s.state, float)
+            if isinstance(s, SensorState) and isinstance(s.state, float)
         ]
 
+        assert sensor_count >= 50, (
+            f"Expected at least 50 sensor states, got {sensor_count}"
+        )
         assert len(sensor_states) >= 50, (
             f"Expected at least 50 sensor states, got {len(sensor_states)}"
         )
diff --git a/tests/integration/test_host_mode_sensor.py b/tests/integration/test_host_mode_sensor.py
index f0c938da1c..8c1e9f5d51 100644
--- a/tests/integration/test_host_mode_sensor.py
+++ b/tests/integration/test_host_mode_sensor.py
@@ -4,6 +4,7 @@ from __future__ import annotations
 
 import asyncio
 
+import aioesphomeapi
 from aioesphomeapi import EntityState
 import pytest
 
@@ -18,16 +19,17 @@ async def test_host_mode_with_sensor(
 ) -> None:
     """Test Host mode with a sensor component."""
     # Write, compile and run the ESPHome device, then connect to API
+    loop = asyncio.get_running_loop()
     async with run_compiled(yaml_config), api_client_connected() as client:
         # Subscribe to state changes
         states: dict[int, EntityState] = {}
-        sensor_future: asyncio.Future[EntityState] = asyncio.Future()
+        sensor_future: asyncio.Future[EntityState] = loop.create_future()
 
         def on_state(state: EntityState) -> None:
             states[state.key] = state
             # If this is our sensor with value 42.0, resolve the future
             if (
-                hasattr(state, "state")
+                isinstance(state, aioesphomeapi.SensorState)
                 and state.state == 42.0
                 and not sensor_future.done()
             ):
@@ -47,3 +49,23 @@ async def test_host_mode_with_sensor(
         # Verify the sensor state
         assert test_sensor_state.state == 42.0
         assert len(states) > 0, "No states received"
+
+        # Verify the optimized fields are working correctly
+        # Get entity info to check accuracy_decimals, state_class, etc.
+        entities, _ = await client.list_entities_services()
+        sensor_info: aioesphomeapi.SensorInfo | None = None
+        for entity in entities:
+            if isinstance(entity, aioesphomeapi.SensorInfo):
+                sensor_info = entity
+                break
+
+        assert sensor_info is not None, "Sensor entity info not found"
+        assert sensor_info.accuracy_decimals == 2, (
+            f"Expected accuracy_decimals=2, got {sensor_info.accuracy_decimals}"
+        )
+        assert sensor_info.state_class == 1, (
+            f"Expected state_class=1 (measurement), got {sensor_info.state_class}"
+        )
+        assert sensor_info.force_update is True, (
+            f"Expected force_update=True, got {sensor_info.force_update}"
+        )
diff --git a/tests/integration/test_legacy_area.py b/tests/integration/test_legacy_area.py
new file mode 100644
index 0000000000..d10a01ec6a
--- /dev/null
+++ b/tests/integration/test_legacy_area.py
@@ -0,0 +1,41 @@
+"""Integration test for legacy string-based area configuration."""
+
+from __future__ import annotations
+
+import pytest
+
+from .types import APIClientConnectedFactory, RunCompiledFunction
+
+
+@pytest.mark.asyncio
+async def test_legacy_area(
+    yaml_config: str,
+    run_compiled: RunCompiledFunction,
+    api_client_connected: APIClientConnectedFactory,
+) -> None:
+    """Test legacy string-based area configuration."""
+    async with run_compiled(yaml_config), api_client_connected() as client:
+        # Get device info which includes areas
+        device_info = await client.device_info()
+        assert device_info is not None
+
+        # Verify the area is reported (should be converted to structured format)
+        areas = device_info.areas
+        assert len(areas) == 1, f"Expected exactly 1 area, got {len(areas)}"
+
+        # Find the area - should be slugified from "Master Bedroom"
+        area = areas[0]
+        assert area.name == "Master Bedroom", (
+            f"Expected area name 'Master Bedroom', got '{area.name}'"
+        )
+
+        # Verify area.id is set (it should be a hash)
+        assert area.area_id > 0, "Area ID should be a positive hash value"
+
+        # The suggested_area field should be set for backward compatibility
+        assert device_info.suggested_area == "Master Bedroom", (
+            f"Expected suggested_area to be 'Master Bedroom', got '{device_info.suggested_area}'"
+        )
+
+        # Verify deprecated warning would have been logged during compilation
+        # (We can't check logs directly in integration tests, but the code should work)
diff --git a/tests/integration/test_light_calls.py b/tests/integration/test_light_calls.py
new file mode 100644
index 0000000000..1c56bbbf9e
--- /dev/null
+++ b/tests/integration/test_light_calls.py
@@ -0,0 +1,190 @@
+"""Integration test for all light call combinations.
+
+Tests that LightCall handles all possible light operations correctly
+including RGB, color temperature, effects, transitions, and flash.
+"""
+
+import asyncio
+from typing import Any
+
+from aioesphomeapi import LightState
+import pytest
+
+from .types import APIClientConnectedFactory, RunCompiledFunction
+
+
+@pytest.mark.asyncio
+async def test_light_calls(
+    yaml_config: str,
+    run_compiled: RunCompiledFunction,
+    api_client_connected: APIClientConnectedFactory,
+) -> None:
+    """Test all possible LightCall operations and combinations."""
+    async with run_compiled(yaml_config), api_client_connected() as client:
+        # Track state changes with futures
+        state_futures: dict[int, asyncio.Future[Any]] = {}
+        states: dict[int, Any] = {}
+
+        def on_state(state: Any) -> None:
+            states[state.key] = state
+            if state.key in state_futures and not state_futures[state.key].done():
+                state_futures[state.key].set_result(state)
+
+        client.subscribe_states(on_state)
+
+        # Get the light entities
+        entities = await client.list_entities_services()
+        lights = [e for e in entities[0] if e.object_id.startswith("test_")]
+        assert len(lights) >= 2  # Should have RGBCW and RGB lights
+
+        rgbcw_light = next(light for light in lights if "RGBCW" in light.name)
+        rgb_light = next(light for light in lights if "RGB Light" in light.name)
+
+        async def wait_for_state_change(key: int, timeout: float = 1.0) -> Any:
+            """Wait for a state change for the given entity key."""
+            loop = asyncio.get_event_loop()
+            state_futures[key] = loop.create_future()
+            try:
+                return await asyncio.wait_for(state_futures[key], timeout)
+            finally:
+                state_futures.pop(key, None)
+
+        # Test all individual parameters first
+
+        # Test 1: state only
+        client.light_command(key=rgbcw_light.key, state=True)
+        state = await wait_for_state_change(rgbcw_light.key)
+        assert state.state is True
+
+        # Test 2: brightness only
+        client.light_command(key=rgbcw_light.key, brightness=0.5)
+        state = await wait_for_state_change(rgbcw_light.key)
+        assert state.brightness == pytest.approx(0.5)
+
+        # Test 3: color_brightness only
+        client.light_command(key=rgbcw_light.key, color_brightness=0.8)
+        state = await wait_for_state_change(rgbcw_light.key)
+        assert state.color_brightness == pytest.approx(0.8)
+
+        # Test 4-7: RGB values must be set together via rgb parameter
+        client.light_command(key=rgbcw_light.key, rgb=(0.7, 0.3, 0.9))
+        state = await wait_for_state_change(rgbcw_light.key)
+        assert state.red == pytest.approx(0.7, abs=0.1)
+        assert state.green == pytest.approx(0.3, abs=0.1)
+        assert state.blue == pytest.approx(0.9, abs=0.1)
+
+        # Test 7: white value
+        client.light_command(key=rgbcw_light.key, white=0.6)
+        state = await wait_for_state_change(rgbcw_light.key)
+        # White might need more tolerance or might not be directly settable
+        if isinstance(state, LightState) and state.white is not None:
+            assert state.white == pytest.approx(0.6, abs=0.1)
+
+        # Test 8: color_temperature only
+        client.light_command(key=rgbcw_light.key, color_temperature=300)
+        state = await wait_for_state_change(rgbcw_light.key)
+        assert state.color_temperature == pytest.approx(300)
+
+        # Test 9: cold_white only
+        client.light_command(key=rgbcw_light.key, cold_white=0.8)
+        state = await wait_for_state_change(rgbcw_light.key)
+        assert state.cold_white == pytest.approx(0.8)
+
+        # Test 10: warm_white only
+        client.light_command(key=rgbcw_light.key, warm_white=0.2)
+        state = await wait_for_state_change(rgbcw_light.key)
+        assert state.warm_white == pytest.approx(0.2)
+
+        # Test 11: transition_length with state change
+        client.light_command(key=rgbcw_light.key, state=False, transition_length=0.1)
+        state = await wait_for_state_change(rgbcw_light.key)
+        assert state.state is False
+
+        # Test 12: flash_length
+        client.light_command(key=rgbcw_light.key, state=True, flash_length=0.2)
+        state = await wait_for_state_change(rgbcw_light.key)
+        # Flash starts
+        assert state.state is True
+        # Wait for flash to end
+        state = await wait_for_state_change(rgbcw_light.key)
+
+        # Test 13: effect only
+        # First ensure light is on
+        client.light_command(key=rgbcw_light.key, state=True)
+        state = await wait_for_state_change(rgbcw_light.key)
+        # Now set effect
+        client.light_command(key=rgbcw_light.key, effect="Random Effect")
+        state = await wait_for_state_change(rgbcw_light.key)
+        assert state.effect == "Random Effect"
+
+        # Test 14: stop effect
+        client.light_command(key=rgbcw_light.key, effect="None")
+        state = await wait_for_state_change(rgbcw_light.key)
+        assert state.effect == "None"
+
+        # Test 15: color_mode parameter
+        client.light_command(
+            key=rgbcw_light.key, state=True, color_mode=5
+        )  # COLD_WARM_WHITE
+        state = await wait_for_state_change(rgbcw_light.key)
+        assert state.state is True
+
+        # Now test common combinations
+
+        # Test 16: RGB combination (set_rgb) - RGB values get normalized
+        client.light_command(key=rgbcw_light.key, rgb=(1.0, 0.0, 0.5))
+        state = await wait_for_state_change(rgbcw_light.key)
+        # RGB values get normalized - in this case red is already 1.0
+        assert state.red == pytest.approx(1.0, abs=0.1)
+        assert state.green == pytest.approx(0.0, abs=0.1)
+        assert state.blue == pytest.approx(0.5, abs=0.1)
+
+        # Test 17: Multiple RGB changes to test transitions
+        client.light_command(key=rgbcw_light.key, rgb=(0.2, 0.8, 0.4))
+        state = await wait_for_state_change(rgbcw_light.key)
+        # RGB values get normalized so green (highest) becomes 1.0
+        # Expected: (0.2/0.8, 0.8/0.8, 0.4/0.8) = (0.25, 1.0, 0.5)
+        assert state.red == pytest.approx(0.25, abs=0.01)
+        assert state.green == pytest.approx(1.0, abs=0.01)
+        assert state.blue == pytest.approx(0.5, abs=0.01)
+
+        # Test 18: State + brightness + transition
+        client.light_command(
+            key=rgbcw_light.key, state=True, brightness=0.7, transition_length=0.1
+        )
+        state = await wait_for_state_change(rgbcw_light.key)
+        assert state.state is True
+        assert state.brightness == pytest.approx(0.7)
+
+        # Test 19: RGB + brightness + color_brightness
+        client.light_command(
+            key=rgb_light.key,
+            state=True,
+            brightness=0.8,
+            color_brightness=0.9,
+            rgb=(0.2, 0.4, 0.6),
+        )
+        state = await wait_for_state_change(rgb_light.key)
+        assert state.state is True
+        assert state.brightness == pytest.approx(0.8)
+
+        # Test 20: Color temp + cold/warm white
+        client.light_command(
+            key=rgbcw_light.key, color_temperature=250, cold_white=0.7, warm_white=0.3
+        )
+        state = await wait_for_state_change(rgbcw_light.key)
+        assert state.color_temperature == pytest.approx(250)
+
+        # Test 21: Turn RGB light off
+        client.light_command(key=rgb_light.key, state=False)
+        state = await wait_for_state_change(rgb_light.key)
+        assert state.state is False
+
+        # Final cleanup - turn all lights off
+        for light in lights:
+            client.light_command(
+                key=light.key,
+                state=False,
+            )
+            state = await wait_for_state_change(light.key)
+            assert state.state is False
diff --git a/tests/integration/test_loop_disable_enable.py b/tests/integration/test_loop_disable_enable.py
new file mode 100644
index 0000000000..d5f868aa93
--- /dev/null
+++ b/tests/integration/test_loop_disable_enable.py
@@ -0,0 +1,207 @@
+"""Integration test for loop disable/enable functionality."""
+
+from __future__ import annotations
+
+import asyncio
+from pathlib import Path
+import re
+
+import pytest
+
+from .types import APIClientConnectedFactory, RunCompiledFunction
+
+
+@pytest.mark.asyncio
+async def test_loop_disable_enable(
+    yaml_config: str,
+    run_compiled: RunCompiledFunction,
+    api_client_connected: APIClientConnectedFactory,
+) -> None:
+    """Test that components can disable and enable their loop() method."""
+    # Get the absolute path to the external components directory
+    external_components_path = str(
+        Path(__file__).parent / "fixtures" / "external_components"
+    )
+
+    # Replace the placeholder in the YAML config with the actual path
+    yaml_config = yaml_config.replace(
+        "EXTERNAL_COMPONENT_PATH", external_components_path
+    )
+
+    # Track log messages and events
+    log_messages: list[str] = []
+
+    # Event fired when self_disable_10 component disables itself after 10 loops
+    self_disable_10_disabled = asyncio.Event()
+    # Event fired when normal_component reaches 10 loops
+    normal_component_10_loops = asyncio.Event()
+    # Event fired when redundant_enable component tests enabling when already enabled
+    redundant_enable_tested = asyncio.Event()
+    # Event fired when redundant_disable component tests disabling when already disabled
+    redundant_disable_tested = asyncio.Event()
+    # Event fired when self_disable_10 component is re-enabled and runs again (count > 10)
+    self_disable_10_re_enabled = asyncio.Event()
+    # Events for ISR component testing
+    isr_component_disabled = asyncio.Event()
+    isr_component_re_enabled = asyncio.Event()
+    isr_component_pure_re_enabled = asyncio.Event()
+
+    # Track loop counts for components
+    self_disable_10_counts: list[int] = []
+    normal_component_counts: list[int] = []
+    isr_component_counts: list[int] = []
+
+    def on_log_line(line: str) -> None:
+        """Process each log line from the process output."""
+        # Strip ANSI color codes
+        clean_line = re.sub(r"\x1b\[[0-9;]*m", "", line)
+
+        if (
+            "loop_test_component" not in clean_line
+            and "loop_test_isr_component" not in clean_line
+        ):
+            return
+
+        log_messages.append(clean_line)
+
+        # Track specific events using the cleaned line
+        if "[self_disable_10]" in clean_line:
+            if "Loop count:" in clean_line:
+                # Extract loop count
+                try:
+                    count = int(clean_line.split("Loop count: ")[1])
+                    self_disable_10_counts.append(count)
+                    # Check if component was re-enabled (count > 10)
+                    if count > 10:
+                        self_disable_10_re_enabled.set()
+                except (IndexError, ValueError):
+                    pass
+            elif "Disabling self after 10 loops" in clean_line:
+                self_disable_10_disabled.set()
+
+        elif "[normal_component]" in clean_line and "Loop count:" in clean_line:
+            try:
+                count = int(clean_line.split("Loop count: ")[1])
+                normal_component_counts.append(count)
+                if count >= 10:
+                    normal_component_10_loops.set()
+            except (IndexError, ValueError):
+                pass
+
+        elif (
+            "[redundant_enable]" in clean_line
+            and "Testing enable when already enabled" in clean_line
+        ):
+            redundant_enable_tested.set()
+
+        elif (
+            "[redundant_disable]" in clean_line
+            and "Testing disable when will be disabled" in clean_line
+        ):
+            redundant_disable_tested.set()
+
+        # ISR component events
+        elif "[isr_test]" in clean_line:
+            if "ISR component loop count:" in clean_line:
+                count = int(clean_line.split("ISR component loop count: ")[1])
+                isr_component_counts.append(count)
+            elif "Disabling after 5 loops" in clean_line:
+                isr_component_disabled.set()
+            elif "Running after ISR re-enable!" in clean_line:
+                isr_component_re_enabled.set()
+            elif "Running after pure ISR re-enable!" in clean_line:
+                isr_component_pure_re_enabled.set()
+
+    # Write, compile and run the ESPHome device with log callback
+    async with (
+        run_compiled(yaml_config, line_callback=on_log_line),
+        api_client_connected() as client,
+    ):
+        # Verify we can connect and get device info
+        device_info = await client.device_info()
+        assert device_info is not None
+        assert device_info.name == "loop-test"
+
+        # Wait for self_disable_10 to disable itself
+        try:
+            await asyncio.wait_for(self_disable_10_disabled.wait(), timeout=10.0)
+        except asyncio.TimeoutError:
+            pytest.fail("self_disable_10 did not disable itself within 10 seconds")
+
+        # Verify it ran at least 10 times before disabling
+        assert len([c for c in self_disable_10_counts if c <= 10]) == 10, (
+            f"Expected exactly 10 loops before disable, got {[c for c in self_disable_10_counts if c <= 10]}"
+        )
+        assert self_disable_10_counts[:10] == list(range(1, 11)), (
+            f"Expected first 10 counts to be 1-10, got {self_disable_10_counts[:10]}"
+        )
+
+        # Wait for normal_component to run at least 10 times
+        try:
+            await asyncio.wait_for(normal_component_10_loops.wait(), timeout=10.0)
+        except asyncio.TimeoutError:
+            pytest.fail(
+                f"normal_component did not reach 10 loops within timeout, got {len(normal_component_counts)}"
+            )
+
+        # Wait for redundant operation tests
+        try:
+            await asyncio.wait_for(redundant_enable_tested.wait(), timeout=10.0)
+        except asyncio.TimeoutError:
+            pytest.fail("redundant_enable did not test enabling when already enabled")
+
+        try:
+            await asyncio.wait_for(redundant_disable_tested.wait(), timeout=10.0)
+        except asyncio.TimeoutError:
+            pytest.fail(
+                "redundant_disable did not test disabling when will be disabled"
+            )
+
+        # Wait to see if self_disable_10 gets re-enabled
+        try:
+            await asyncio.wait_for(self_disable_10_re_enabled.wait(), timeout=5.0)
+        except asyncio.TimeoutError:
+            pytest.fail("self_disable_10 was not re-enabled within 5 seconds")
+
+        # Component was re-enabled - verify it ran more times
+        later_self_disable_counts = [c for c in self_disable_10_counts if c > 10]
+        assert later_self_disable_counts, (
+            "self_disable_10 was re-enabled but did not run additional times"
+        )
+
+        # Test ISR component functionality
+        # Wait for ISR component to disable itself after 5 loops
+        try:
+            await asyncio.wait_for(isr_component_disabled.wait(), timeout=3.0)
+        except asyncio.TimeoutError:
+            pytest.fail("ISR component did not disable itself within 3 seconds")
+
+        # Verify it ran exactly 5 times before disabling
+        first_run_counts = [c for c in isr_component_counts if c <= 5]
+        assert len(first_run_counts) == 5, (
+            f"Expected 5 loops before disable, got {first_run_counts}"
+        )
+
+        # Wait for component to be re-enabled by periodic ISR simulation and run again
+        try:
+            await asyncio.wait_for(isr_component_re_enabled.wait(), timeout=2.0)
+        except asyncio.TimeoutError:
+            pytest.fail("ISR component was not re-enabled after ISR call")
+
+        # Verify it's running again after ISR enable
+        count_after_isr = len(isr_component_counts)
+        assert count_after_isr > 5, (
+            f"Component didn't run after ISR enable: got {count_after_isr} counts total"
+        )
+
+        # Wait for pure ISR enable (no main loop enable) to work
+        try:
+            await asyncio.wait_for(isr_component_pure_re_enabled.wait(), timeout=2.0)
+        except asyncio.TimeoutError:
+            pytest.fail("ISR component was not re-enabled by pure ISR call")
+
+        # Verify it ran after pure ISR enable
+        final_count = len(isr_component_counts)
+        assert final_count > 10, (
+            f"Component didn't run after pure ISR enable: got {final_count} counts total"
+        )
diff --git a/tests/integration/test_scheduler_bulk_cleanup.py b/tests/integration/test_scheduler_bulk_cleanup.py
new file mode 100644
index 0000000000..08ff293b84
--- /dev/null
+++ b/tests/integration/test_scheduler_bulk_cleanup.py
@@ -0,0 +1,122 @@
+"""Test that triggers the bulk cleanup path when to_remove_ > MAX_LOGICALLY_DELETED_ITEMS."""
+
+import asyncio
+from pathlib import Path
+import re
+
+from aioesphomeapi import UserService
+import pytest
+
+from .types import APIClientConnectedFactory, RunCompiledFunction
+
+
+@pytest.mark.asyncio
+async def test_scheduler_bulk_cleanup(
+    yaml_config: str,
+    run_compiled: RunCompiledFunction,
+    api_client_connected: APIClientConnectedFactory,
+) -> None:
+    """Test that bulk cleanup path is triggered when many items are cancelled."""
+
+    # Get the absolute path to the external components directory
+    external_components_path = str(
+        Path(__file__).parent / "fixtures" / "external_components"
+    )
+
+    # Replace the placeholder in the YAML config with the actual path
+    yaml_config = yaml_config.replace(
+        "EXTERNAL_COMPONENT_PATH", external_components_path
+    )
+
+    # Create a future to signal test completion
+    loop = asyncio.get_event_loop()
+    test_complete_future: asyncio.Future[None] = loop.create_future()
+    bulk_cleanup_triggered = False
+    cleanup_stats: dict[str, int] = {
+        "removed": 0,
+        "before": 0,
+        "after": 0,
+    }
+    post_cleanup_executed = 0
+
+    def on_log_line(line: str) -> None:
+        nonlocal bulk_cleanup_triggered, post_cleanup_executed
+
+        # Look for logs indicating bulk cleanup was triggered
+        # The actual cleanup happens silently, so we track the cancel operations
+        if "Successfully cancelled" in line and "timeouts" in line:
+            match = re.search(r"Successfully cancelled (\d+) timeouts", line)
+            if match and int(match.group(1)) > 10:
+                bulk_cleanup_triggered = True
+
+        # Track cleanup statistics
+        match = re.search(r"Bulk cleanup triggered: removed (\d+) items", line)
+        if match:
+            cleanup_stats["removed"] = int(match.group(1))
+
+        match = re.search(r"Items before cleanup: (\d+), after: (\d+)", line)
+        if match:
+            cleanup_stats["before"] = int(match.group(1))
+            cleanup_stats["after"] = int(match.group(2))
+
+        # Track post-cleanup timeout executions
+        if "Post-cleanup timeout" in line and "executed correctly" in line:
+            match = re.search(r"Post-cleanup timeout (\d+) executed correctly", line)
+            if match:
+                post_cleanup_executed += 1
+
+        # Check for final test completion
+        if (
+            "All post-cleanup timeouts completed - test finished" in line
+            and not test_complete_future.done()
+        ):
+            test_complete_future.set_result(None)
+
+    async with (
+        run_compiled(yaml_config, line_callback=on_log_line),
+        api_client_connected() as client,
+    ):
+        # Verify we can connect
+        device_info = await client.device_info()
+        assert device_info is not None
+        assert device_info.name == "scheduler-bulk-cleanup"
+
+        # List entities and services
+        _, services = await asyncio.wait_for(
+            client.list_entities_services(), timeout=5.0
+        )
+
+        # Find our test service
+        trigger_bulk_cleanup_service: UserService | None = None
+        for service in services:
+            if service.name == "trigger_bulk_cleanup":
+                trigger_bulk_cleanup_service = service
+                break
+
+        assert trigger_bulk_cleanup_service is not None, (
+            "trigger_bulk_cleanup service not found"
+        )
+
+        # Execute the test
+        client.execute_service(trigger_bulk_cleanup_service, {})
+
+        # Wait for test completion
+        try:
+            await asyncio.wait_for(test_complete_future, timeout=10.0)
+        except asyncio.TimeoutError:
+            pytest.fail("Bulk cleanup test timed out")
+
+        # Verify bulk cleanup was triggered
+        assert bulk_cleanup_triggered, (
+            "Bulk cleanup path was not triggered - MAX_LOGICALLY_DELETED_ITEMS threshold not reached"
+        )
+
+        # Verify cleanup statistics
+        assert cleanup_stats["removed"] > 10, (
+            f"Expected more than 10 items removed, got {cleanup_stats['removed']}"
+        )
+
+        # Verify scheduler still works after bulk cleanup
+        assert post_cleanup_executed == 5, (
+            f"Expected 5 post-cleanup timeouts to execute, but {post_cleanup_executed} executed"
+        )
diff --git a/tests/integration/test_scheduler_defer_cancel.py b/tests/integration/test_scheduler_defer_cancel.py
new file mode 100644
index 0000000000..923cf946c4
--- /dev/null
+++ b/tests/integration/test_scheduler_defer_cancel.py
@@ -0,0 +1,94 @@
+"""Test that defer() with the same name cancels previous defers."""
+
+import asyncio
+
+from aioesphomeapi import EntityState, Event, EventInfo, UserService
+import pytest
+
+from .types import APIClientConnectedFactory, RunCompiledFunction
+
+
+@pytest.mark.asyncio
+async def test_scheduler_defer_cancel(
+    yaml_config: str,
+    run_compiled: RunCompiledFunction,
+    api_client_connected: APIClientConnectedFactory,
+) -> None:
+    """Test that defer() with the same name cancels previous defers."""
+
+    async with run_compiled(yaml_config), api_client_connected() as client:
+        # Verify we can connect
+        device_info = await client.device_info()
+        assert device_info is not None
+        assert device_info.name == "scheduler-defer-cancel"
+
+        # List entities and services
+        entity_info, services = await asyncio.wait_for(
+            client.list_entities_services(), timeout=5.0
+        )
+
+        # Find our test entities
+        test_complete_entity: EventInfo | None = None
+        test_result_entity: EventInfo | None = None
+
+        for entity in entity_info:
+            if isinstance(entity, EventInfo):
+                if entity.object_id == "test_complete":
+                    test_complete_entity = entity
+                elif entity.object_id == "test_result":
+                    test_result_entity = entity
+
+        assert test_complete_entity is not None, "test_complete event not found"
+        assert test_result_entity is not None, "test_result event not found"
+
+        # Find our test service
+        test_defer_cancel_service: UserService | None = None
+        for service in services:
+            if service.name == "test_defer_cancel":
+                test_defer_cancel_service = service
+
+        assert test_defer_cancel_service is not None, (
+            "test_defer_cancel service not found"
+        )
+
+        # Get the event loop
+        loop = asyncio.get_running_loop()
+
+        # Subscribe to states
+        test_complete_future: asyncio.Future[bool] = loop.create_future()
+        test_result_future: asyncio.Future[int] = loop.create_future()
+
+        def on_state(state: EntityState) -> None:
+            if not isinstance(state, Event):
+                return
+
+            if (
+                state.key == test_complete_entity.key
+                and state.event_type == "test_finished"
+                and not test_complete_future.done()
+            ):
+                test_complete_future.set_result(True)
+                return
+
+            if state.key == test_result_entity.key and not test_result_future.done():
+                # Event type should be "defer_executed_X" where X is the defer number
+                if state.event_type.startswith("defer_executed_"):
+                    defer_num = int(state.event_type.split("_")[-1])
+                    test_result_future.set_result(defer_num)
+
+        client.subscribe_states(on_state)
+
+        # Execute the test
+        client.execute_service(test_defer_cancel_service, {})
+
+        # Wait for test completion
+        try:
+            await asyncio.wait_for(test_complete_future, timeout=10.0)
+            executed_defer = await asyncio.wait_for(test_result_future, timeout=1.0)
+        except asyncio.TimeoutError:
+            pytest.fail("Test did not complete within timeout")
+
+        # Verify that only defer 10 was executed
+        assert executed_defer == 10, (
+            f"Expected defer 10 to execute, got {executed_defer}"
+        )
diff --git a/tests/integration/test_scheduler_defer_cancel_regular.py b/tests/integration/test_scheduler_defer_cancel_regular.py
new file mode 100644
index 0000000000..57b7134feb
--- /dev/null
+++ b/tests/integration/test_scheduler_defer_cancel_regular.py
@@ -0,0 +1,90 @@
+"""Test that a deferred timeout cancels a regular timeout with the same name."""
+
+import asyncio
+
+from aioesphomeapi import UserService
+import pytest
+
+from .types import APIClientConnectedFactory, RunCompiledFunction
+
+
+@pytest.mark.asyncio
+async def test_scheduler_defer_cancels_regular(
+    yaml_config: str,
+    run_compiled: RunCompiledFunction,
+    api_client_connected: APIClientConnectedFactory,
+) -> None:
+    """Test that set_timeout(name, 0) cancels a previously scheduled set_timeout(name, delay)."""
+
+    # Create a future to signal test completion
+    loop = asyncio.get_running_loop()
+    test_complete_future: asyncio.Future[None] = loop.create_future()
+
+    # Track log messages
+    log_messages: list[str] = []
+    error_detected = False
+
+    def on_log_line(line: str) -> None:
+        nonlocal error_detected
+        if "TEST" in line:
+            log_messages.append(line)
+
+        if "ERROR: Regular timeout executed" in line:
+            error_detected = True
+
+        if "Test complete" in line and not test_complete_future.done():
+            test_complete_future.set_result(None)
+
+    async with (
+        run_compiled(yaml_config, line_callback=on_log_line),
+        api_client_connected() as client,
+    ):
+        # Verify we can connect
+        device_info = await client.device_info()
+        assert device_info is not None
+        assert device_info.name == "scheduler-defer-cancel-regular"
+
+        # List services
+        _, services = await asyncio.wait_for(
+            client.list_entities_services(), timeout=5.0
+        )
+
+        # Find our test service
+        test_service: UserService | None = None
+        for service in services:
+            if service.name == "test_defer_cancels_regular":
+                test_service = service
+                break
+
+        assert test_service is not None, "test_defer_cancels_regular service not found"
+
+        # Execute the test
+        client.execute_service(test_service, {})
+
+        # Wait for test completion
+        try:
+            await asyncio.wait_for(test_complete_future, timeout=5.0)
+        except asyncio.TimeoutError:
+            pytest.fail(f"Test timed out. Log messages: {log_messages}")
+
+        # Verify results
+        assert not error_detected, (
+            f"Regular timeout should have been cancelled but it executed! Logs: {log_messages}"
+        )
+
+        # Verify the deferred timeout executed
+        assert any(
+            "SUCCESS: Deferred timeout executed" in msg for msg in log_messages
+        ), f"Deferred timeout should have executed. Logs: {log_messages}"
+
+        # Verify the expected sequence of events
+        assert any(
+            "Starting defer cancels regular timeout test" in msg for msg in log_messages
+        )
+        assert any(
+            "Scheduled regular timeout with 100ms delay" in msg for msg in log_messages
+        )
+        assert any(
+            "Scheduled deferred timeout - should cancel regular timeout" in msg
+            for msg in log_messages
+        )
diff --git a/tests/integration/test_scheduler_defer_fifo_simple.py b/tests/integration/test_scheduler_defer_fifo_simple.py
new file mode 100644
index 0000000000..eb4058fedd
--- /dev/null
+++ b/tests/integration/test_scheduler_defer_fifo_simple.py
@@ -0,0 +1,117 @@
+"""Simple test that defer() maintains FIFO order."""
+
+import asyncio
+
+from aioesphomeapi import EntityState, Event, EventInfo, UserService
+import pytest
+
+from .types import APIClientConnectedFactory, RunCompiledFunction
+
+
+@pytest.mark.asyncio
+async def test_scheduler_defer_fifo_simple(
+    yaml_config: str,
+    run_compiled: RunCompiledFunction,
+    api_client_connected: APIClientConnectedFactory,
+) -> None:
+    """Test that defer() maintains FIFO order with a simple test."""
+
+    async with run_compiled(yaml_config), api_client_connected() as client:
+        # Verify we can connect
+        device_info = await client.device_info()
+        assert device_info is not None
+        assert device_info.name == "scheduler-defer-fifo-simple"
+
+        # List entities and services
+        entity_info, services = await asyncio.wait_for(
+            client.list_entities_services(), timeout=5.0
+        )
+
+        # Find our test entities
+        test_complete_entity: EventInfo | None = None
+        test_result_entity: EventInfo | None = None
+
+        for entity in entity_info:
+            if isinstance(entity, EventInfo):
+                if entity.object_id == "test_complete":
+                    test_complete_entity = entity
+                elif entity.object_id == "test_result":
+                    test_result_entity = entity
+
+        assert test_complete_entity is not None, "test_complete event not found"
+        assert test_result_entity is not None, "test_result event not found"
+
+        # Find our test services
+        test_set_timeout_service: UserService | None = None
+        test_defer_service: UserService | None = None
+        for service in services:
+            if service.name == "test_set_timeout":
+                test_set_timeout_service = service
+            elif service.name == "test_defer":
+                test_defer_service = service
+
+        assert test_set_timeout_service is not None, (
+            "test_set_timeout service not found"
+        )
+        assert test_defer_service is not None, "test_defer service not found"
+
+        # Get the event loop
+        loop = asyncio.get_running_loop()
+
+        # Subscribe to states
+        # (events are delivered as EventStates through subscribe_states)
+        test_complete_future: asyncio.Future[bool] = loop.create_future()
+        test_result_future: asyncio.Future[bool] = loop.create_future()
+
+        def on_state(state: EntityState) -> None:
+            if not isinstance(state, Event):
+                return
+
+            if (
+                state.key == test_complete_entity.key
+                and state.event_type == "test_finished"
+                and not test_complete_future.done()
+            ):
+                test_complete_future.set_result(True)
+                return
+
+            if state.key == test_result_entity.key and not test_result_future.done():
+                if state.event_type == "passed":
+                    test_result_future.set_result(True)
+                elif state.event_type == "failed":
+                    test_result_future.set_result(False)
+
+        client.subscribe_states(on_state)
+
+        # Test 1: Test set_timeout(0)
+        client.execute_service(test_set_timeout_service, {})
+
+        # Wait for first test completion
+        try:
+            await asyncio.wait_for(test_complete_future, timeout=5.0)
+            test1_passed = await asyncio.wait_for(test_result_future, timeout=1.0)
+        except asyncio.TimeoutError:
+            pytest.fail("Test set_timeout(0) did not complete within 5 seconds")
+
+        assert test1_passed is True, (
+            "set_timeout(0) FIFO test failed - items executed out of order"
+        )
+
+        # Reset futures for second test
+        test_complete_future = loop.create_future()
+        test_result_future = loop.create_future()
+
+        # Test 2: Test defer()
+        client.execute_service(test_defer_service, {})
+
+        # Wait for second test completion
+        try:
+            await asyncio.wait_for(test_complete_future, timeout=5.0)
+            test2_passed = await asyncio.wait_for(test_result_future, timeout=1.0)
+        except asyncio.TimeoutError:
+            pytest.fail("Test defer() did not complete within 5 seconds")
+
+        # Verify the test passed
+        assert test2_passed is True, (
+            "defer() FIFO test failed - items executed out of order"
+        )
diff --git a/tests/integration/test_scheduler_defer_stress.py b/tests/integration/test_scheduler_defer_stress.py
new file mode 100644
index 0000000000..d546b7132f
--- /dev/null
+++ b/tests/integration/test_scheduler_defer_stress.py
@@ -0,0 +1,137 @@
+"""Stress test for defer() thread safety with multiple threads."""
+
+import asyncio
+from pathlib import Path
+import re
+
+from aioesphomeapi import UserService
+import pytest
+
+from .types import APIClientConnectedFactory, RunCompiledFunction
+
+
+@pytest.mark.asyncio
+async def test_scheduler_defer_stress(
+    yaml_config: str,
+    run_compiled: RunCompiledFunction,
+    api_client_connected: APIClientConnectedFactory,
+) -> None:
+    """Test that defer() doesn't crash when called rapidly from multiple threads."""
+
+    # Get the absolute path to the external components directory
+    external_components_path = str(
+        Path(__file__).parent / "fixtures" / "external_components"
+    )
+
+    # Replace the placeholder in the YAML config with the actual path
+    yaml_config = yaml_config.replace(
+        "EXTERNAL_COMPONENT_PATH", external_components_path
+    )
+
+    # Create a future to signal test completion
+    loop = asyncio.get_event_loop()
+    test_complete_future: asyncio.Future[None] = loop.create_future()
+
+    # Track executed defers and their order
+    executed_defers: set[int] = set()
+    thread_executions: dict[
+        int, list[int]
+    ] = {}  # thread_id -> list of indices in execution order
+    fifo_violations: list[str] = []
+
+    def on_log_line(line: str) -> None:
+        # Track all executed defers with thread and index info
+        match = re.search(r"Executed defer (\d+) \(thread (\d+), index (\d+)\)", line)
+        if not match:
+            return
+
+        defer_id = int(match.group(1))
+        thread_id = int(match.group(2))
+        index = int(match.group(3))
+
+        executed_defers.add(defer_id)
+
+        # Track execution order per thread
+        if thread_id not in thread_executions:
+            thread_executions[thread_id] = []
+
+        # Check FIFO ordering within thread
+        if thread_executions[thread_id] and thread_executions[thread_id][-1] >= index:
+            fifo_violations.append(
+                f"Thread {thread_id}: index {index} executed after "
+                f"{thread_executions[thread_id][-1]}"
+            )
+
+        thread_executions[thread_id].append(index)
+
+        # Check if we've executed all 1000 defers (0-999)
+        if len(executed_defers) == 1000 and not test_complete_future.done():
+            test_complete_future.set_result(None)
+
+    async with (
+        run_compiled(yaml_config, line_callback=on_log_line),
+        api_client_connected() as client,
+    ):
+        # Verify we can connect
+        device_info = await client.device_info()
+        assert device_info is not None
+        assert device_info.name == "scheduler-defer-stress-test"
+
+        # List entities and services
+        entity_info, services = await asyncio.wait_for(
+            client.list_entities_services(), timeout=5.0
+        )
+
+        # Find our test service
+        run_stress_test_service: UserService | None = None
+        for service in services:
+            if service.name == "run_stress_test":
+                run_stress_test_service = service
+                break
+
+        assert run_stress_test_service is not None, "run_stress_test service not found"
+
+        # Call the run_stress_test service to start the test
+        client.execute_service(run_stress_test_service, {})
+
+        # Wait for all defers to execute (should be quick)
+        try:
+            await asyncio.wait_for(test_complete_future, timeout=5.0)
+        except asyncio.TimeoutError:
+            # Report how many we got
+            pytest.fail(
+                f"Stress test timed out. Only {len(executed_defers)} of "
+                f"1000 defers executed. Missing IDs: "
+                f"{sorted(set(range(1000)) - executed_defers)[:10]}..."
+            )
+
+        # Verify all defers executed
+        assert len(executed_defers) == 1000, (
+            f"Expected 1000 defers, got {len(executed_defers)}"
+        )
+
+        # Verify we have all IDs from 0-999
+        expected_ids = set(range(1000))
+        missing_ids = expected_ids - executed_defers
+        assert not missing_ids, f"Missing defer IDs: {sorted(missing_ids)}"
+
+        # Verify FIFO ordering was maintained within each thread
+        assert not fifo_violations, "FIFO ordering violations detected:\n" + "\n".join(
+            fifo_violations[:10]
+        )
+
+        # Verify each thread executed all its defers in order
+        for thread_id, indices in thread_executions.items():
+            assert len(indices) == 100, (
+                f"Thread {thread_id} executed {len(indices)} defers, expected 100"
+            )
+            # Indices should be 0-99 in ascending order
+            assert indices == list(range(100)), (
+                f"Thread {thread_id} executed indices out of order: {indices[:10]}..."
+            )
+
+        # If we got here without crashing and with proper ordering, the test passed
+        assert True, (
+            "Test completed successfully - all 1000 defers executed with "
+            "FIFO ordering preserved"
+        )
diff --git a/tests/integration/test_scheduler_heap_stress.py b/tests/integration/test_scheduler_heap_stress.py
new file mode 100644
index 0000000000..3c757bfc9d
--- /dev/null
+++ b/tests/integration/test_scheduler_heap_stress.py
@@ -0,0 +1,140 @@
+"""Stress test for heap scheduler thread safety with multiple threads."""
+
+import asyncio
+from pathlib import Path
+import re
+
+from aioesphomeapi import UserService
+import pytest
+
+from .types import APIClientConnectedFactory, RunCompiledFunction
+
+
+@pytest.mark.asyncio
+async def test_scheduler_heap_stress(
+    yaml_config: str,
+    run_compiled: RunCompiledFunction,
+    api_client_connected: APIClientConnectedFactory,
+) -> None:
+    """Test that set_timeout/set_interval doesn't crash when called rapidly from multiple threads."""
+
+    # Get the absolute path to the external components directory
+    external_components_path = str(
+        Path(__file__).parent / "fixtures" / "external_components"
+    )
+
+    # Replace the placeholder in the YAML config with the actual path
+    yaml_config = yaml_config.replace(
+        "EXTERNAL_COMPONENT_PATH", external_components_path
+    )
+
+    # Create a future to signal test completion
+    loop = asyncio.get_running_loop()
+    test_complete_future: asyncio.Future[None] = loop.create_future()
+
+    # Track executed timeouts/intervals and their order
+    executed_callbacks: set[int] = set()
+    thread_executions: dict[
+        int, list[int]
+    ] = {}  # thread_id -> list of indices in execution order
+    callback_types: dict[int, str] = {}  # callback_id -> "timeout" or "interval"
+
+    def on_log_line(line: str) -> None:
+        # Track all executed callbacks with thread and index info
+        match = re.search(
+            r"Executed (timeout|interval) (\d+) \(thread (\d+), index (\d+)\)", line
+        )
+        if not match:
+            # Also check for the completion message
+            if "All threads finished" in line and "Created 1000 callbacks" in line:
+                # Give scheduler some time to execute callbacks
+                pass
+            return
+
+        callback_type = match.group(1)
+        callback_id = int(match.group(2))
+        thread_id = int(match.group(3))
+        index = int(match.group(4))
+
+        # Only count each callback ID once (intervals might fire multiple times)
+        if callback_id not in executed_callbacks:
+            executed_callbacks.add(callback_id)
+            callback_types[callback_id] = callback_type
+
+        # Track execution order per thread
+        if thread_id not in thread_executions:
+            thread_executions[thread_id] = []
+
+        # Only append if this is a new execution for this thread
+        if index not in thread_executions[thread_id]:
+            thread_executions[thread_id].append(index)
+
+        # Check if we've executed all 1000 callbacks (0-999)
+        if len(executed_callbacks) >= 1000 and not test_complete_future.done():
+            test_complete_future.set_result(None)
+
+    async with (
+        run_compiled(yaml_config, line_callback=on_log_line),
+        api_client_connected() as client,
+    ):
+        # Verify we can connect
+        device_info = await client.device_info()
+        assert device_info is not None
+        assert device_info.name == "scheduler-heap-stress-test"
+
+        # List entities and services
+        _, services = await asyncio.wait_for(
+            client.list_entities_services(), timeout=5.0
+        )
+
+        # Find our test service
+        run_stress_test_service: UserService | None = None
+        for service in services:
+            if service.name == "run_heap_stress_test":
+                run_stress_test_service = service
+                break
+
+        assert run_stress_test_service is not None, (
+            "run_heap_stress_test service not found"
+        )
+
+        # Call the run_heap_stress_test service to start the test
+        client.execute_service(run_stress_test_service, {})
+
+        # Wait for all callbacks to execute (should be quick, but give more time for scheduling)
+        try:
+            await asyncio.wait_for(test_complete_future, timeout=60.0)
+        except asyncio.TimeoutError:
+            # Report how many we got
+            pytest.fail(
+                f"Stress test timed out. Only {len(executed_callbacks)} of "
+                f"1000 callbacks executed. Missing IDs: "
+                f"{sorted(set(range(1000)) - executed_callbacks)[:10]}..."
+            )
+
+        # Verify all callbacks executed
+        assert len(executed_callbacks) == 1000, (
+            f"Expected 1000 callbacks, got {len(executed_callbacks)}"
+        )
+
+        # Verify we have all IDs from 0-999
+        expected_ids = set(range(1000))
+        missing_ids = expected_ids - executed_callbacks
+        assert not missing_ids, f"Missing callback IDs: {sorted(missing_ids)}"
+
+        # Verify we have a mix of timeouts and intervals
+        timeout_count = sum(1 for t in callback_types.values() if t == "timeout")
+        interval_count = sum(1 for t in callback_types.values() if t == "interval")
+        assert timeout_count > 0, "No timeouts were executed"
+        assert interval_count > 0, "No intervals were executed"
+
+        # Verify each thread executed callbacks
+        for thread_id, indices in thread_executions.items():
+            assert len(indices) == 100, (
+                f"Thread {thread_id} executed {len(indices)} callbacks, expected 100"
+            )
+        # Total should be 1000 callbacks
+        total_callbacks = timeout_count + interval_count
+        assert total_callbacks == 1000, (
+            f"Expected 1000 total callbacks but got {total_callbacks}"
+        )
diff --git a/tests/integration/test_scheduler_rapid_cancellation.py b/tests/integration/test_scheduler_rapid_cancellation.py
new file mode 100644
index 0000000000..90577f36f1
--- /dev/null
+++ b/tests/integration/test_scheduler_rapid_cancellation.py
@@ -0,0 +1,142 @@
+"""Rapid cancellation test - schedule and immediately cancel timeouts with string names."""
+
+import asyncio
+from pathlib import Path
+import re
+
+from aioesphomeapi import UserService
+import pytest
+
+from .types import APIClientConnectedFactory, RunCompiledFunction
+
+
+@pytest.mark.asyncio
+async def test_scheduler_rapid_cancellation(
+    yaml_config: str,
+    run_compiled: RunCompiledFunction,
+    api_client_connected: APIClientConnectedFactory,
+) -> None:
+    """Test rapid schedule/cancel cycles that might expose race conditions."""
+
+    # Get the absolute path to the external components directory
+    external_components_path = str(
+        Path(__file__).parent / "fixtures" / "external_components"
+    )
+
+    # Replace the placeholder in the YAML config with the actual path
+    yaml_config = yaml_config.replace(
+        "EXTERNAL_COMPONENT_PATH", external_components_path
+    )
+
+    # Create a future to signal test completion
+    loop = asyncio.get_running_loop()
+    test_complete_future: asyncio.Future[None] = loop.create_future()
+
+    # Track test progress
+    test_stats = {
+        "log_count": 0,
+        "errors": [],
+        "summary_scheduled": None,
+        "final_scheduled": 0,
+        "final_executed": 0,
+        "final_implicit_cancellations": 0,
+    }
+
+    def on_log_line(line: str) -> None:
+        # Count log lines
+        test_stats["log_count"] += 1
+
+        # Check for errors (only ERROR level, not WARN)
+        if "ERROR" in line:
+            test_stats["errors"].append(line)
+
+        # Parse summary statistics
+        if "All threads completed. Scheduled:" in line:
+            # Extract the scheduled count from the summary
+            if match := re.search(r"Scheduled: (\d+)", line):
+                test_stats["summary_scheduled"] = int(match.group(1))
+        elif "Total scheduled:" in line:
+            if match := re.search(r"Total scheduled: (\d+)", line):
+                test_stats["final_scheduled"] = int(match.group(1))
+        elif "Total executed:" in line:
+            if match := re.search(r"Total executed: (\d+)", line):
+                test_stats["final_executed"] = int(match.group(1))
+        elif "Implicit cancellations (replaced):" in line:
+            if match := re.search(r"Implicit cancellations \(replaced\): (\d+)", line):
+                test_stats["final_implicit_cancellations"] = int(match.group(1))
+
+        # Check for crash indicators
+        if any(
+            indicator in line.lower()
+            for indicator in ["segfault", "abort", "assertion", "heap corruption"]
+        ):
+            if not test_complete_future.done():
+                test_complete_future.set_exception(Exception(f"Crash detected: {line}"))
+            return
+
+        # Check for completion - wait for final message after all stats are logged
+        if (
+            "Test finished - all statistics reported" in line
+            and not test_complete_future.done()
+        ):
+            test_complete_future.set_result(None)
+
+    async with (
+        run_compiled(yaml_config, line_callback=on_log_line),
+        api_client_connected() as client,
+    ):
+        # Verify we can connect
+        device_info = await client.device_info()
+        assert device_info is not None
+        assert device_info.name == "sched-rapid-cancel-test"
+
+        # List entities and services
+        _, services = await asyncio.wait_for(
+            client.list_entities_services(), timeout=5.0
+        )
+
+        # Find our test service
+        run_test_service: UserService | None = None
+        for service in services:
+            if service.name == "run_rapid_cancellation_test":
+                run_test_service = service
+                break
+
+        assert run_test_service is not None, (
+            "run_rapid_cancellation_test service not found"
+        )
+
+        # Call the service to start the test
+        client.execute_service(run_test_service, {})
+
+        # Wait for test to complete with timeout
+        try:
+            await asyncio.wait_for(test_complete_future, timeout=10.0)
+        except asyncio.TimeoutError:
+            pytest.fail(f"Test timed out. Stats: {test_stats}")
+
+        # Check for any errors
+        assert len(test_stats["errors"]) == 0, (
+            f"Errors detected: {test_stats['errors']}"
+        )
+
+        # Check that we received log messages
+        assert test_stats["log_count"] > 0, "No log messages received"
+
+        # Check the summary line to verify all threads scheduled their operations
+        assert test_stats["summary_scheduled"] == 400, (
+            f"Expected summary to show 400 scheduled operations but got {test_stats['summary_scheduled']}"
+        )
+
+        # Check final statistics
+        assert test_stats["final_scheduled"] == 400, (
+            f"Expected final stats to show 400 scheduled but got {test_stats['final_scheduled']}"
+        )
+
+        assert test_stats["final_executed"] == 10, (
+            f"Expected final stats to show 10 executed but got {test_stats['final_executed']}"
+        )
+
+        assert test_stats["final_implicit_cancellations"] == 390, (
+            f"Expected final stats to show 390 implicit cancellations but got {test_stats['final_implicit_cancellations']}"
+        )
diff --git a/tests/integration/test_scheduler_recursive_timeout.py b/tests/integration/test_scheduler_recursive_timeout.py
new file mode 100644
index 0000000000..c015978e15
--- /dev/null
+++ b/tests/integration/test_scheduler_recursive_timeout.py
@@ -0,0 +1,101 @@
+"""Test for recursive timeout scheduling - scheduling timeouts from within timeout callbacks."""
+
+import asyncio
+from pathlib import Path
+
+from aioesphomeapi import UserService
+import pytest
+
+from .types import APIClientConnectedFactory, RunCompiledFunction
+
+
+@pytest.mark.asyncio
+async def test_scheduler_recursive_timeout(
+    yaml_config: str,
+    run_compiled: RunCompiledFunction,
+    api_client_connected: APIClientConnectedFactory,
+) -> None:
+    """Test that scheduling timeouts from within timeout callbacks works correctly."""
+
+    # Get the absolute path to the external components directory
+    external_components_path = str(
+        Path(__file__).parent / "fixtures" / "external_components"
+    )
+
+    # Replace the placeholder in the YAML config with the actual path
+    yaml_config = yaml_config.replace(
+        "EXTERNAL_COMPONENT_PATH", external_components_path
+    )
+
+    # Create a future to signal test completion
+    loop = asyncio.get_running_loop()
+    test_complete_future: asyncio.Future[None] = loop.create_future()
+
+    # Track execution sequence
+    execution_sequence: list[str] = []
+    expected_sequence = [
+        "initial_timeout",
+        "nested_timeout_1",
+        "nested_timeout_2",
+        "test_complete",
+    ]
+
+    def on_log_line(line: str) -> None:
+        # Track execution sequence
+        if "Executing initial timeout" in line:
+            execution_sequence.append("initial_timeout")
+        elif "Executing nested timeout 1" in line:
+            execution_sequence.append("nested_timeout_1")
+        elif "Executing nested timeout 2" in line:
+            execution_sequence.append("nested_timeout_2")
+        elif "Recursive timeout test complete" in line:
+            execution_sequence.append("test_complete")
+            if not test_complete_future.done():
+                test_complete_future.set_result(None)
+
+    async with (
+        run_compiled(yaml_config, line_callback=on_log_line),
+        api_client_connected() as client,
+    ):
+        # Verify we can connect
+        device_info = await client.device_info()
+        assert device_info is not None
+        assert device_info.name == "sched-recursive-timeout"
+
+        # List entities and services
+        _, services = await asyncio.wait_for(
+            client.list_entities_services(), timeout=5.0
+        )
+
+        # Find our test service
+        run_test_service: UserService | None = None
+        for service in services:
+            if service.name == "run_recursive_timeout_test":
+                run_test_service = service
+                break
+
+        assert run_test_service is not None, (
+            "run_recursive_timeout_test service not found"
+        )
+
+        # Call the service to start the test
+        client.execute_service(run_test_service, {})
+
+        # Wait for test to complete
+        try:
+            await asyncio.wait_for(test_complete_future, timeout=10.0)
+        except asyncio.TimeoutError:
+            pytest.fail(
+                f"Recursive timeout test timed out. Got sequence: {execution_sequence}"
+            )
+
+        # Verify execution sequence
+        assert execution_sequence == expected_sequence, (
+            f"Execution sequence mismatch. Expected {expected_sequence}, "
+            f"got {execution_sequence}"
+        )
+
+        # Verify we got exactly 4 events (Initial + Level 1 + Level 2 + Complete)
+        assert len(execution_sequence) == 4, (
+            f"Expected 4 events but got {len(execution_sequence)}"
+        )
diff --git a/tests/integration/test_scheduler_simultaneous_callbacks.py b/tests/integration/test_scheduler_simultaneous_callbacks.py
new file mode 100644
index 0000000000..f5120ce4ce
--- /dev/null
+++ b/tests/integration/test_scheduler_simultaneous_callbacks.py
@@ -0,0 +1,123 @@
+"""Simultaneous callbacks test - schedule many callbacks for the same time from multiple threads."""
+
+import asyncio
+from pathlib import Path
+import re
+
+from aioesphomeapi import UserService
+import pytest
+
+from .types import APIClientConnectedFactory, RunCompiledFunction
+
+
+@pytest.mark.asyncio
+async def test_scheduler_simultaneous_callbacks(
+    yaml_config: str,
+    run_compiled: RunCompiledFunction,
+    api_client_connected: APIClientConnectedFactory,
+) -> None:
+    """Test scheduling many callbacks for the exact same time from multiple threads."""
+
+    # Get the absolute path to the external components directory
+    external_components_path = str(
+        Path(__file__).parent / "fixtures" / "external_components"
+    )
+
+    # Replace the placeholder in the YAML config with the actual path
+    yaml_config = yaml_config.replace(
+        "EXTERNAL_COMPONENT_PATH", external_components_path
+    )
+
+    # Create a future to signal test completion
+    loop = asyncio.get_running_loop()
+    test_complete_future: asyncio.Future[None] = loop.create_future()
+
+    # Track test progress
+    test_stats = {
+        "scheduled": 0,
+        "executed": 0,
+        "expected": 1000,  # 10 threads * 100 callbacks
+        "errors": [],
+    }
+
+    def on_log_line(line: str) -> None:
+        # Track operations
+        if "Scheduled callback" in line:
+            test_stats["scheduled"] += 1
+        elif "Callback executed" in line:
+            test_stats["executed"] += 1
+        elif "ERROR" in line:
+            test_stats["errors"].append(line)
+
+        # Check for crash indicators
+        if any(
+            indicator in line.lower()
+            for indicator in ["segfault", "abort", "assertion", "heap corruption"]
+        ):
+            if not test_complete_future.done():
+                test_complete_future.set_exception(Exception(f"Crash detected: {line}"))
+            return
+
+        # Check for completion with final count
+        if "Final executed count:" in line:
+            # Extract number from log line like: "[07:59:47][I][simultaneous_callbacks:093]: Simultaneous callbacks test complete. Final executed count: 1000"
+            match = re.search(r"Final executed count:\s*(\d+)", line)
+            if match:
+                test_stats["final_count"] = int(match.group(1))
+
+        # Check for completion
+        if (
+            "Simultaneous callbacks test complete" in line
+            and not test_complete_future.done()
+        ):
+            test_complete_future.set_result(None)
+
+    async with (
+        run_compiled(yaml_config, line_callback=on_log_line),
+        api_client_connected() as client,
+    ):
+        # Verify we can connect
+        device_info = await client.device_info()
+        assert device_info is not None
+        assert device_info.name == "sched-simul-callbacks-test"
+
+        # List entities and services
+        _, services = await asyncio.wait_for(
+            client.list_entities_services(), timeout=5.0
+        )
+
+        # Find our test service
+        run_test_service: UserService | None = None
+        for service in services:
+            if service.name == "run_simultaneous_callbacks_test":
+                run_test_service = service
+                break
+
+        assert run_test_service is not None, (
+            "run_simultaneous_callbacks_test service not found"
+        )
+
+        # Call the service to start the test
+        client.execute_service(run_test_service, {})
+
+        # Wait for test to complete
+        try:
+            await asyncio.wait_for(test_complete_future, timeout=30.0)
+        except asyncio.TimeoutError:
+            pytest.fail(f"Simultaneous callbacks test timed out. Stats: {test_stats}")
+
+        # Check for any errors
+        assert len(test_stats["errors"]) == 0, (
+            f"Errors detected: {test_stats['errors']}"
+        )
+
+        # Verify all callbacks executed using the final count from C++
+        final_count = test_stats.get("final_count", 0)
+        assert final_count == test_stats["expected"], (
+            f"Expected {test_stats['expected']} callbacks, but only {final_count} executed"
+        )
+
+        # The final_count is the authoritative count from the C++ component
+        assert final_count == 1000, (
+            f"Expected 1000 executed callbacks but got {final_count}"
+        )
diff --git a/tests/integration/test_scheduler_string_lifetime.py b/tests/integration/test_scheduler_string_lifetime.py
new file mode 100644
index 0000000000..4d77abd954
--- /dev/null
+++ b/tests/integration/test_scheduler_string_lifetime.py
@@ -0,0 +1,169 @@
+"""String lifetime test - verify scheduler handles string destruction correctly."""
+
+import asyncio
+from pathlib import Path
+import re
+
+import pytest
+
+from .types import APIClientConnectedFactory, RunCompiledFunction
+
+
+@pytest.mark.asyncio
+async def test_scheduler_string_lifetime(
+    yaml_config: str,
+    run_compiled: RunCompiledFunction,
+    api_client_connected: APIClientConnectedFactory,
+) -> None:
+    """Test that scheduler correctly handles string lifetimes when strings go out of scope."""
+
+    # Get the absolute path to the external components directory
+    external_components_path = str(
+        Path(__file__).parent / "fixtures" / "external_components"
+    )
+
+    # Replace the placeholder in the YAML config with the actual path
+    yaml_config = yaml_config.replace(
+        "EXTERNAL_COMPONENT_PATH", external_components_path
+    )
+
+    # Create events for synchronization
+    test1_complete = asyncio.Event()
+    test2_complete = asyncio.Event()
+    test3_complete = asyncio.Event()
+    test4_complete = asyncio.Event()
+    test5_complete = asyncio.Event()
+    all_tests_complete = asyncio.Event()
+
+    # Track test progress
+    test_stats = {
+        "tests_passed": 0,
+        "tests_failed": 0,
+        "errors": [],
+        "current_test": None,
+        "test_callbacks_executed": {},
+    }
+
+    def on_log_line(line: str) -> None:
+        # Track test-specific events
+        if "Test 1 complete" in line:
+            test1_complete.set()
+        elif "Test 2 complete" in line:
+            test2_complete.set()
+        elif "Test 3 complete" in line:
+            test3_complete.set()
+        elif "Test 4 complete" in line:
+            test4_complete.set()
+        elif "Test 5 complete" in line:
+            test5_complete.set()
+
+        # Track individual callback executions
+        callback_match = re.search(r"Callback '(.+?)' executed", line)
+        if callback_match:
+            callback_name = callback_match.group(1)
+            test_stats["test_callbacks_executed"][callback_name] = True
+
+        # Track test results from the C++ test output
+        if "Tests passed:" in line and "string_lifetime" in line:
+            # Extract the number from "Tests passed: 32"
+            match = re.search(r"Tests passed:\s*(\d+)", line)
+            if match:
+                test_stats["tests_passed"] = int(match.group(1))
+        elif "Tests failed:" in line and "string_lifetime" in line:
+            match = re.search(r"Tests failed:\s*(\d+)", line)
+            if match:
+                test_stats["tests_failed"] = int(match.group(1))
+        elif "ERROR" in line and "string_lifetime" in line:
+            test_stats["errors"].append(line)
+
+        # Check for memory corruption indicators
+        if any(
+            indicator in line.lower()
+            for indicator in [
+                "use after free",
+                "heap corruption",
+                "segfault",
+                "abort",
+                "assertion",
+                "sanitizer",
+                "bad memory",
+                "invalid pointer",
+            ]
+        ):
+            pytest.fail(f"Memory corruption detected: {line}")
+
+        # Check for completion
+        if "String lifetime tests complete" in line:
+            all_tests_complete.set()
+
+    async with (
+        run_compiled(yaml_config, line_callback=on_log_line),
+        api_client_connected() as client,
+    ):
+        # Verify we can connect
+        device_info = await client.device_info()
+        assert device_info is not None
+        assert device_info.name == "scheduler-string-lifetime-test"
+
+        # List entities and services
+        _, services = await asyncio.wait_for(
+            client.list_entities_services(), timeout=5.0
+        )
+
+        # Find our test services
+        test_services = {}
+        for service in services:
+            if service.name == "run_test1":
+                test_services["test1"] = service
+            elif service.name == "run_test2":
+                test_services["test2"] = service
+            elif service.name == "run_test3":
+                test_services["test3"] = service
+            elif service.name == "run_test4":
+                test_services["test4"] = service
+            elif service.name == "run_test5":
+                test_services["test5"] = service
+            elif service.name == "run_final_check":
+                test_services["final"] = service
+
+        # Ensure all services are found
+        required_services = ["test1", "test2", "test3", "test4", "test5", "final"]
+        for service_name in required_services:
+            assert service_name in test_services, f"{service_name} service not found"
+
+        # Run tests sequentially, waiting for each to complete
+        try:
+            # Test 1
+            client.execute_service(test_services["test1"], {})
+            await asyncio.wait_for(test1_complete.wait(), timeout=5.0)
+
+            # Test 2
+            client.execute_service(test_services["test2"], {})
+            await asyncio.wait_for(test2_complete.wait(), timeout=5.0)
+
+            # Test 3
+            client.execute_service(test_services["test3"], {})
+            await asyncio.wait_for(test3_complete.wait(), timeout=5.0)
+
+            # Test 4
+            client.execute_service(test_services["test4"], {})
+            await asyncio.wait_for(test4_complete.wait(), timeout=5.0)
+
+            # Test 5
+            client.execute_service(test_services["test5"], {})
+            await asyncio.wait_for(test5_complete.wait(), timeout=5.0)
+
+            # Final check
+            client.execute_service(test_services["final"], {})
+            await asyncio.wait_for(all_tests_complete.wait(), timeout=5.0)
+
+        except asyncio.TimeoutError:
+            pytest.fail(f"String lifetime test timed out. Stats: {test_stats}")
+
+        # Check for any errors
+        assert test_stats["tests_failed"] == 0, f"Tests failed: {test_stats['errors']}"
+
+        # Verify we had the expected number of passing tests
+        assert test_stats["tests_passed"] == 30, (
+            f"Expected exactly 30 tests to pass, but got {test_stats['tests_passed']}"
+        )
diff --git a/tests/integration/test_scheduler_string_name_stress.py b/tests/integration/test_scheduler_string_name_stress.py
new file mode 100644
index 0000000000..3045842223
--- /dev/null
+++ b/tests/integration/test_scheduler_string_name_stress.py
@@ -0,0 +1,116 @@
+"""Stress test for heap scheduler with std::string names from multiple threads."""
+
+import asyncio
+from pathlib import Path
+import re
+
+from aioesphomeapi import UserService
+import pytest
+
+from .types import APIClientConnectedFactory, RunCompiledFunction
+
+
+@pytest.mark.asyncio
+async def test_scheduler_string_name_stress(
+    yaml_config: str,
+    run_compiled: RunCompiledFunction,
+    api_client_connected: APIClientConnectedFactory,
+) -> None:
+    """Test that set_timeout/set_interval with std::string names doesn't crash when called from multiple threads."""
+
+    # Get the absolute path to the external components directory
+    external_components_path = str(
+        Path(__file__).parent / "fixtures" / "external_components"
+    )
+
+    # Replace the placeholder in the YAML config with the actual path
+    yaml_config = yaml_config.replace(
+        "EXTERNAL_COMPONENT_PATH", external_components_path
+    )
+
+    # Create a future to signal test completion
+    loop = asyncio.get_running_loop()
+    test_complete_future: asyncio.Future[None] = loop.create_future()
+
+    # Track executed callbacks and any crashes
+    executed_callbacks: set[int] = set()
+    error_messages: list[str] = []
+
+    def on_log_line(line: str) -> None:
+        # Check for crash indicators
+        if any(
+            indicator in line.lower()
+            for indicator in [
+                "segfault",
+                "abort",
+                "assertion",
+                "heap corruption",
+                "use after free",
+            ]
+        ):
+            error_messages.append(line)
+            if not test_complete_future.done():
+                test_complete_future.set_exception(Exception(f"Crash detected: {line}"))
+            return
+
+        # Track executed callbacks
+        match = re.search(r"Executed string-named callback (\d+)", line)
+        if match:
+            callback_id = int(match.group(1))
+            executed_callbacks.add(callback_id)
+
+        # Check for completion
+        if (
+            "String name stress test complete" in line
+            and not test_complete_future.done()
+        ):
+            test_complete_future.set_result(None)
+
+    async with (
+        run_compiled(yaml_config, line_callback=on_log_line),
+        api_client_connected() as client,
+    ):
+        # Verify we can connect
+        device_info = await client.device_info()
+        assert device_info is not None
+        assert device_info.name == "sched-string-name-stress"
+
+        # List entities and services
+        _, services = await asyncio.wait_for(
+            client.list_entities_services(), timeout=5.0
+        )
+
+        # Find our test service
+        run_stress_test_service: UserService | None = None
+        for service in services:
+            if service.name == "run_string_name_stress_test":
+                run_stress_test_service = service
+                break
+
+        assert run_stress_test_service is not None, (
+            "run_string_name_stress_test service not found"
+        )
+
+        # Call the service to start the test
+        client.execute_service(run_stress_test_service, {})
+
+        # Wait for test to complete or crash
+        try:
+            await asyncio.wait_for(test_complete_future, timeout=30.0)
+        except asyncio.TimeoutError:
+            pytest.fail(
+                f"String name stress test timed out. Executed {len(executed_callbacks)} callbacks. "
+                f"This might indicate a deadlock."
+            )
+
+        # Verify no errors occurred (crashes already handled by exception)
+        assert not error_messages, f"Errors detected during test: {error_messages}"
+
+        # Verify we executed all 1000 callbacks (10 threads × 100 callbacks each)
+        assert len(executed_callbacks) == 1000, (
+            f"Expected 1000 callbacks but got {len(executed_callbacks)}"
+        )
+
+        # Verify each callback ID was executed exactly once
+        for i in range(1000):
+            assert i in executed_callbacks, f"Callback {i} was not executed"
diff --git a/tests/integration/test_scheduler_string_test.py b/tests/integration/test_scheduler_string_test.py
new file mode 100644
index 0000000000..f3a36b2db7
--- /dev/null
+++ b/tests/integration/test_scheduler_string_test.py
@@ -0,0 +1,202 @@
+"""Test scheduler string optimization with static and dynamic strings."""
+
+import asyncio
+import re
+
+import pytest
+
+from .types import APIClientConnectedFactory, RunCompiledFunction
+
+
+@pytest.mark.asyncio
+async def test_scheduler_string_test(
+    yaml_config: str,
+    run_compiled: RunCompiledFunction,
+    api_client_connected: APIClientConnectedFactory,
+) -> None:
+    """Test that scheduler handles both static and dynamic strings correctly."""
+    # Track counts
+    timeout_count = 0
+    interval_count = 0
+
+    # Events for each test completion
+    static_timeout_1_fired = asyncio.Event()
+    static_timeout_2_fired = asyncio.Event()
+    static_interval_fired = asyncio.Event()
+    static_interval_cancelled = asyncio.Event()
+    empty_string_timeout_fired = asyncio.Event()
+    static_timeout_cancelled = asyncio.Event()
+    static_defer_1_fired = asyncio.Event()
+    static_defer_2_fired = asyncio.Event()
+    dynamic_timeout_fired = asyncio.Event()
+    dynamic_interval_fired = asyncio.Event()
+    dynamic_defer_fired = asyncio.Event()
+    cancel_test_done = asyncio.Event()
+    final_results_logged = asyncio.Event()
+
+    # Track interval counts
+    static_interval_count = 0
+    dynamic_interval_count = 0
+
+    def on_log_line(line: str) -> None:
+        nonlocal \
+            timeout_count, \
+            interval_count, \
+            static_interval_count, \
+            dynamic_interval_count
+
+        # Strip ANSI color codes
+        clean_line = re.sub(r"\x1b\[[0-9;]*m", "", line)
+
+        # Check for static timeout completions
+        if "Static timeout 1 fired" in clean_line:
+            static_timeout_1_fired.set()
+            timeout_count += 1
+
+        elif "Static timeout 2 fired" in clean_line:
+            static_timeout_2_fired.set()
+            timeout_count += 1
+
+        # Check for static interval
+        elif "Static interval 1 fired" in clean_line:
+            match = re.search(r"count: (\d+)", clean_line)
+            if match:
+                static_interval_count = int(match.group(1))
+                static_interval_fired.set()
+
+        elif "Cancelled static interval 1" in clean_line:
+            static_interval_cancelled.set()
+
+        # Check for empty string timeout
+        elif "Empty string timeout fired" in clean_line:
+            empty_string_timeout_fired.set()
+
+        # Check for static timeout cancellation
+        elif "Cancelled static timeout using const char*" in clean_line:
+            static_timeout_cancelled.set()
+
+        # Check for static defer tests
+        elif "Static defer 1 fired" in clean_line:
+            static_defer_1_fired.set()
+            timeout_count += 1
+
+        elif "Static defer 2 fired" in clean_line:
+            static_defer_2_fired.set()
+            timeout_count += 1
+
+        # Check for dynamic string tests
+        elif "Dynamic timeout fired" in clean_line:
+            dynamic_timeout_fired.set()
+            timeout_count += 1
+
+        elif "Dynamic interval fired" in clean_line:
+            dynamic_interval_count += 1
+            dynamic_interval_fired.set()
+
+        # Check for dynamic defer test
+        elif "Dynamic defer fired" in clean_line:
+            dynamic_defer_fired.set()
+            timeout_count += 1
+
+        # Check for cancel test
+        elif "Cancelled timeout using different string object" in clean_line:
+            cancel_test_done.set()
+
+        # Check for final results
+        elif "Final results" in clean_line:
+            match = re.search(r"Timeouts: (\d+), Intervals: (\d+)", clean_line)
+            if match:
+                timeout_count = int(match.group(1))
+                interval_count = int(match.group(2))
+                final_results_logged.set()
+
+    async with (
+        run_compiled(yaml_config, line_callback=on_log_line),
+        api_client_connected() as client,
+    ):
+        # Verify we can connect
+        device_info = await client.device_info()
+        assert device_info is not None
+        assert device_info.name == "scheduler-string-test"
+
+        # Wait for static string tests
+        try:
+            await asyncio.wait_for(static_timeout_1_fired.wait(), timeout=0.5)
+        except asyncio.TimeoutError:
+            pytest.fail("Static timeout 1 did not fire within 0.5 seconds")
+
+        try:
+            await asyncio.wait_for(static_timeout_2_fired.wait(), timeout=0.5)
+        except asyncio.TimeoutError:
+            pytest.fail("Static timeout 2 did not fire within 0.5 seconds")
+
+        try:
+            await asyncio.wait_for(static_interval_fired.wait(), timeout=1.0)
+        except asyncio.TimeoutError:
+            pytest.fail("Static interval did not fire within 1 second")
+
+        try:
+            await asyncio.wait_for(static_interval_cancelled.wait(), timeout=2.0)
+        except asyncio.TimeoutError:
+            pytest.fail("Static interval was not cancelled within 2 seconds")
+
+        # Verify static interval ran at least 3 times
+        assert static_interval_count >= 2, (
+            f"Expected static interval to run at least 3 times, got {static_interval_count + 1}"
+        )
+
+        # Verify static timeout was cancelled
+        assert static_timeout_cancelled.is_set(), (
+            "Static timeout should have been cancelled"
+        )
+
+        # Wait for static defer tests
+        try:
+            await asyncio.wait_for(static_defer_1_fired.wait(), timeout=0.5)
+        except asyncio.TimeoutError:
+            pytest.fail("Static defer 1 did not fire within 0.5 seconds")
+
+        try:
+            await asyncio.wait_for(static_defer_2_fired.wait(), timeout=0.5)
+        except asyncio.TimeoutError:
+            pytest.fail("Static defer 2 did not fire within 0.5 seconds")
+
+        # Wait for dynamic string tests
+        try:
+            await asyncio.wait_for(dynamic_timeout_fired.wait(), timeout=1.0)
+        except asyncio.TimeoutError:
+            pytest.fail("Dynamic timeout did not fire within 1 second")
+
+        try:
+            await asyncio.wait_for(dynamic_interval_fired.wait(), timeout=1.5)
+        except asyncio.TimeoutError:
+            pytest.fail("Dynamic interval did not fire within 1.5 seconds")
+
+        # Wait for dynamic defer test
+        try:
+            await asyncio.wait_for(dynamic_defer_fired.wait(), timeout=1.0)
+        except asyncio.TimeoutError:
+            pytest.fail("Dynamic defer did not fire within 1 second")
+
+        # Wait for cancel test
+        try:
+            await asyncio.wait_for(cancel_test_done.wait(), timeout=1.0)
+        except asyncio.TimeoutError:
+            pytest.fail("Cancel test did not complete within 1 second")
+
+        # Wait for final results
+        try:
+            await asyncio.wait_for(final_results_logged.wait(), timeout=4.0)
+        except asyncio.TimeoutError:
+            pytest.fail("Final results were not logged within 4 seconds")
+
+        # Verify results
+        assert timeout_count >= 6, (
+            f"Expected at least 6 timeouts (including defers), got {timeout_count}"
+        )
+        assert interval_count >= 3, (
+            f"Expected at least 3 interval fires, got {interval_count}"
+        )
+
+        # Empty string timeout DOES fire (scheduler accepts empty names)
+        assert empty_string_timeout_fired.is_set(), "Empty string timeout should fire"
diff --git a/tests/integration/types.py b/tests/integration/types.py
index 6fc3e9435e..5e4bfaa29d 100644
--- a/tests/integration/types.py
+++ b/tests/integration/types.py
@@ -13,7 +13,19 @@ from aioesphomeapi import APIClient
 ConfigWriter = Callable[[str, str | None], Awaitable[Path]]
 CompileFunction = Callable[[Path], Awaitable[Path]]
 RunFunction = Callable[[Path], Awaitable[asyncio.subprocess.Process]]
-RunCompiledFunction = Callable[[str, str | None], AbstractAsyncContextManager[None]]
+
+
+class RunCompiledFunction(Protocol):
+    """Protocol for run_compiled function with optional line callback."""
+
+    def __call__(  # noqa: E704
+        self,
+        yaml_content: str,
+        filename: str | None = None,
+        line_callback: Callable[[str], None] | None = None,
+    ) -> AbstractAsyncContextManager[None]: ...
+
+
 WaitFunction = Callable[[APIClient, float], Awaitable[bool]]
 
 
diff --git a/tests/test_build_components/build_components_base.esp32-p4-idf.yaml b/tests/test_build_components/build_components_base.esp32-p4-idf.yaml
index e2b975f643..9e4f0ddd61 100644
--- a/tests/test_build_components/build_components_base.esp32-p4-idf.yaml
+++ b/tests/test_build_components/build_components_base.esp32-p4-idf.yaml
@@ -15,4 +15,3 @@ packages:
     file: $component_test_file
     vars:
       component_test_file: $component_test_file
-
diff --git a/tests/test_build_components/build_components_base.ln882x-ard.yaml b/tests/test_build_components/build_components_base.ln882x-ard.yaml
new file mode 100644
index 0000000000..80fc6690f9
--- /dev/null
+++ b/tests/test_build_components/build_components_base.ln882x-ard.yaml
@@ -0,0 +1,15 @@
+esphome:
+  name: componenttestespln882x
+  friendly_name: $component_name
+
+ln882x:
+  board: generic-ln882hki
+
+logger:
+  level: VERY_VERBOSE
+
+packages:
+  component_under_test: !include
+    file: $component_test_file
+    vars:
+      component_test_file: $component_test_file
diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py
index 955869b799..aac5a642f6 100644
--- a/tests/unit_tests/conftest.py
+++ b/tests/unit_tests/conftest.py
@@ -14,6 +14,8 @@ import sys
 
 import pytest
 
+from esphome.core import CORE
+
 here = Path(__file__).parent
 
 # Configure location of package root
@@ -21,6 +23,13 @@ package_root = here.parent.parent
 sys.path.insert(0, package_root.as_posix())
 
 
+@pytest.fixture(autouse=True)
+def reset_core():
+    """Reset CORE after each test."""
+    yield
+    CORE.reset()
+
+
 @pytest.fixture
 def fixture_path() -> Path:
     """
diff --git a/tests/unit_tests/core/__init__.py b/tests/unit_tests/core/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tests/unit_tests/core/common.py b/tests/unit_tests/core/common.py
new file mode 100644
index 0000000000..1848d5397b
--- /dev/null
+++ b/tests/unit_tests/core/common.py
@@ -0,0 +1,33 @@
+"""Common test utilities for core unit tests."""
+
+from collections.abc import Callable
+from pathlib import Path
+from unittest.mock import patch
+
+from esphome import config, yaml_util
+from esphome.config import Config
+from esphome.core import CORE
+
+
+def load_config_from_yaml(
+    yaml_file: Callable[[str], str], yaml_content: str
+) -> Config | None:
+    """Load configuration from YAML content."""
+    yaml_path = yaml_file(yaml_content)
+    parsed_yaml = yaml_util.load_yaml(yaml_path)
+
+    # Mock yaml_util.load_yaml to return our parsed content
+    with (
+        patch.object(yaml_util, "load_yaml", return_value=parsed_yaml),
+        patch.object(CORE, "config_path", yaml_path),
+    ):
+        return config.read_config({})
+
+
+def load_config_from_fixture(
+    yaml_file: Callable[[str], str], fixture_name: str, fixtures_dir: Path
+) -> Config | None:
+    """Load configuration from a fixture file."""
+    fixture_path = fixtures_dir / fixture_name
+    yaml_content = fixture_path.read_text()
+    return load_config_from_yaml(yaml_file, yaml_content)
diff --git a/tests/unit_tests/core/conftest.py b/tests/unit_tests/core/conftest.py
new file mode 100644
index 0000000000..60d6738ce9
--- /dev/null
+++ b/tests/unit_tests/core/conftest.py
@@ -0,0 +1,18 @@
+"""Shared fixtures for core unit tests."""
+
+from collections.abc import Callable
+from pathlib import Path
+
+import pytest
+
+
+@pytest.fixture
+def yaml_file(tmp_path: Path) -> Callable[[str], str]:
+    """Create a temporary YAML file for testing."""
+
+    def _yaml_file(content: str) -> str:
+        yaml_path = tmp_path / "test.yaml"
+        yaml_path.write_text(content)
+        return str(yaml_path)
+
+    return _yaml_file
diff --git a/tests/unit_tests/core/test_config.py b/tests/unit_tests/core/test_config.py
new file mode 100644
index 0000000000..46e3b513d7
--- /dev/null
+++ b/tests/unit_tests/core/test_config.py
@@ -0,0 +1,225 @@
+"""Unit tests for core config functionality including areas and devices."""
+
+from collections.abc import Callable
+from pathlib import Path
+from typing import Any
+
+import pytest
+
+from esphome import config_validation as cv, core
+from esphome.const import CONF_AREA, CONF_AREAS, CONF_DEVICES
+from esphome.core.config import Area, validate_area_config
+
+from .common import load_config_from_fixture
+
+FIXTURES_DIR = Path(__file__).parent.parent / "fixtures" / "core" / "config"
+
+
+def test_validate_area_config_with_string() -> None:
+    """Test that string area config is converted to structured format."""
+    result = validate_area_config("Living Room")
+
+    assert isinstance(result, dict)
+    assert "id" in result
+    assert "name" in result
+    assert result["name"] == "Living Room"
+    assert isinstance(result["id"], core.ID)
+    assert result["id"].is_declaration
+    assert not result["id"].is_manual
+
+
+def test_validate_area_config_with_dict() -> None:
+    """Test that structured area config passes through unchanged."""
+    area_id = cv.declare_id(Area)("test_area")
+    input_config: dict[str, Any] = {
+        "id": area_id,
+        "name": "Test Area",
+    }
+
+    result = validate_area_config(input_config)
+
+    assert result == input_config
+    assert result["id"] == area_id
+    assert result["name"] == "Test Area"
+
+
+def test_device_with_valid_area_id(yaml_file: Callable[[str], str]) -> None:
+    """Test that device with valid area_id works correctly."""
+    result = load_config_from_fixture(yaml_file, "valid_area_device.yaml", FIXTURES_DIR)
+    assert result is not None
+
+    esphome_config = result["esphome"]
+
+    # Verify areas were parsed correctly
+    assert CONF_AREAS in esphome_config
+    areas = esphome_config[CONF_AREAS]
+    assert len(areas) == 1
+    assert areas[0]["id"].id == "bedroom_area"
+    assert areas[0]["name"] == "Bedroom"
+
+    # Verify devices were parsed correctly
+    assert CONF_DEVICES in esphome_config
+    devices = esphome_config[CONF_DEVICES]
+    assert len(devices) == 1
+    assert devices[0]["id"].id == "test_device"
+    assert devices[0]["name"] == "Test Device"
+    assert devices[0]["area_id"].id == "bedroom_area"
+
+
+def test_multiple_areas_and_devices(yaml_file: Callable[[str], str]) -> None:
+    """Test multiple areas and devices configuration."""
+    result = load_config_from_fixture(
+        yaml_file, "multiple_areas_devices.yaml", FIXTURES_DIR
+    )
+    assert result is not None
+
+    esphome_config = result["esphome"]
+
+    # Verify main area
+    assert CONF_AREA in esphome_config
+    main_area = esphome_config[CONF_AREA]
+    assert main_area["id"].id == "main_area"
+    assert main_area["name"] == "Main Area"
+
+    # Verify additional areas
+    assert CONF_AREAS in esphome_config
+    areas = esphome_config[CONF_AREAS]
+    assert len(areas) == 2
+    area_ids = {area["id"].id for area in areas}
+    assert area_ids == {"area1", "area2"}
+
+    # Verify devices
+    assert CONF_DEVICES in esphome_config
+    devices = esphome_config[CONF_DEVICES]
+    assert len(devices) == 3
+
+    # Check device-area associations
+    device_area_map = {dev["id"].id: dev["area_id"].id for dev in devices}
+    assert device_area_map == {
+        "device1": "main_area",
+        "device2": "area1",
+        "device3": "area2",
+    }
+
+
+def test_legacy_string_area(
+    yaml_file: Callable[[str], str], caplog: pytest.LogCaptureFixture
+) -> None:
+    """Test legacy string area configuration with deprecation warning."""
+    result = load_config_from_fixture(
+        yaml_file, "legacy_string_area.yaml", FIXTURES_DIR
+    )
+    assert result is not None
+
+    esphome_config = result["esphome"]
+
+    # Verify the string was converted to structured format
+    assert CONF_AREA in esphome_config
+    area = esphome_config[CONF_AREA]
+    assert isinstance(area, dict)
+    assert area["name"] == "Living Room"
+    assert isinstance(area["id"], core.ID)
+    assert area["id"].is_declaration
+    assert not area["id"].is_manual
+
+
+def test_area_id_collision(
+    yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str]
+) -> None:
+    """Test that duplicate area IDs are detected."""
+    result = load_config_from_fixture(yaml_file, "area_id_collision.yaml", FIXTURES_DIR)
+    assert result is None
+
+    # Check for the specific error message in stdout
+    captured = capsys.readouterr()
+    # Exact duplicates are now caught by IDPassValidationStep
+    assert "ID duplicate_id redefined! Check esphome->area->id." in captured.out
+
+
+def test_device_without_area(yaml_file: Callable[[str], str]) -> None:
+    """Test that devices without area_id work correctly."""
+    result = load_config_from_fixture(
+        yaml_file, "device_without_area.yaml", FIXTURES_DIR
+    )
+    assert result is not None
+
+    esphome_config = result["esphome"]
+
+    # Verify device was parsed
+    assert CONF_DEVICES in esphome_config
+    devices = esphome_config[CONF_DEVICES]
+    assert len(devices) == 1
+
+    device = devices[0]
+    assert device["id"].id == "test_device"
+    assert device["name"] == "Test Device"
+
+    # Verify no area_id is present
+    assert "area_id" not in device
+
+
+def test_device_with_invalid_area_id(
+    yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str]
+) -> None:
+    """Test that device with non-existent area_id fails validation."""
+    result = load_config_from_fixture(
+        yaml_file, "device_invalid_area.yaml", FIXTURES_DIR
+    )
+    assert result is None
+
+    # Check for the specific error message in stdout
+    captured = capsys.readouterr()
+    assert (
+        "Couldn't find ID 'nonexistent_area'. Please check you have defined an ID with that name in your configuration."
+        in captured.out
+    )
+
+
+def test_device_id_hash_collision(
+    yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str]
+) -> None:
+    """Test that device IDs with hash collisions are detected."""
+    result = load_config_from_fixture(
+        yaml_file, "device_id_collision.yaml", FIXTURES_DIR
+    )
+    assert result is None
+
+    # Check for the specific error message about hash collision
+    captured = capsys.readouterr()
+    # The error message shows the ID that collides and includes the hash value
+    assert (
+        "Device ID 'd6ka' with hash 3082558663 collides with existing device ID 'test_2258'"
+        in captured.out
+    )
+
+
+def test_area_id_hash_collision(
+    yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str]
+) -> None:
+    """Test that area IDs with hash collisions are detected."""
+    result = load_config_from_fixture(
+        yaml_file, "area_id_hash_collision.yaml", FIXTURES_DIR
+    )
+    assert result is None
+
+    # Check for the specific error message about hash collision
+    captured = capsys.readouterr()
+    # The error message shows the ID that collides and includes the hash value
+    assert (
+        "Area ID 'd6ka' with hash 3082558663 collides with existing area ID 'test_2258'"
+        in captured.out
+    )
+
+
+def test_device_duplicate_id(
+    yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str]
+) -> None:
+    """Test that duplicate device IDs are detected by IDPassValidationStep."""
+    result = load_config_from_fixture(
+        yaml_file, "device_duplicate_id.yaml", FIXTURES_DIR
+    )
+    assert result is None
+
+    # Check for the specific error message from IDPassValidationStep
+    captured = capsys.readouterr()
+    assert "ID duplicate_device redefined!" in captured.out
diff --git a/tests/unit_tests/core/test_entity_helpers.py b/tests/unit_tests/core/test_entity_helpers.py
new file mode 100644
index 0000000000..0dcdd84507
--- /dev/null
+++ b/tests/unit_tests/core/test_entity_helpers.py
@@ -0,0 +1,607 @@
+"""Test get_base_entity_object_id function matches C++ behavior."""
+
+from collections.abc import Callable, Generator
+from pathlib import Path
+import re
+from typing import Any
+
+import pytest
+
+from esphome.config_validation import Invalid
+from esphome.const import CONF_DEVICE_ID, CONF_DISABLED_BY_DEFAULT, CONF_ICON, CONF_NAME
+from esphome.core import CORE, ID, entity_helpers
+from esphome.core.entity_helpers import get_base_entity_object_id, setup_entity
+from esphome.cpp_generator import MockObj
+from esphome.helpers import sanitize, snake_case
+
+from .common import load_config_from_fixture
+
+# Pre-compiled regex pattern for extracting object IDs from expressions
+OBJECT_ID_PATTERN = re.compile(r'\.set_object_id\(["\'](.*?)["\']\)')
+
+FIXTURES_DIR = Path(__file__).parent.parent / "fixtures" / "core" / "entity_helpers"
+
+
+@pytest.fixture(autouse=True)
+def restore_core_state() -> Generator[None, None, None]:
+    """Save and restore CORE state for tests."""
+    original_name = CORE.name
+    original_friendly_name = CORE.friendly_name
+    yield
+    CORE.name = original_name
+    CORE.friendly_name = original_friendly_name
+
+
+def test_with_entity_name() -> None:
+    """Test when entity has its own name - should use entity name."""
+    # Simple name
+    assert get_base_entity_object_id("Temperature Sensor", None) == "temperature_sensor"
+    assert (
+        get_base_entity_object_id("Temperature Sensor", "Device Name")
+        == "temperature_sensor"
+    )
+    # Even with device name, entity name takes precedence
+    assert (
+        get_base_entity_object_id("Temperature Sensor", "Device Name", "Sub Device")
+        == "temperature_sensor"
+    )
+
+    # Name with special characters
+    assert (
+        get_base_entity_object_id("Temp!@#$%^&*()Sensor", None)
+        == "temp__________sensor"
+    )
+    assert get_base_entity_object_id("Temp-Sensor_123", None) == "temp-sensor_123"
+
+    # Already snake_case
+    assert get_base_entity_object_id("temperature_sensor", None) == "temperature_sensor"
+
+    # Mixed case
+    assert get_base_entity_object_id("TemperatureSensor", None) == "temperaturesensor"
+    assert get_base_entity_object_id("TEMPERATURE SENSOR", None) == "temperature_sensor"
+
+
+def test_empty_name_with_device_name() -> None:
+    """Test when entity has empty name and is on a sub-device - should use device name."""
+    # C++ behavior: when has_own_name is false and device is set, uses device->get_name()
+    assert (
+        get_base_entity_object_id("", "Friendly Device", "Sub Device 1")
+        == "sub_device_1"
+    )
+    assert (
+        get_base_entity_object_id("", "Kitchen Controller", "controller_1")
+        == "controller_1"
+    )
+    assert get_base_entity_object_id("", None, "Test-Device_123") == "test-device_123"
+
+
+def test_empty_name_with_friendly_name() -> None:
+    """Test when entity has empty name and no device - should use friendly name."""
+    # C++ behavior: when has_own_name is false, uses App.get_friendly_name()
+    assert get_base_entity_object_id("", "Friendly Device") == "friendly_device"
+    assert get_base_entity_object_id("", "Kitchen Controller") == "kitchen_controller"
+    assert get_base_entity_object_id("", "Test-Device_123") == "test-device_123"
+
+    # Special characters in friendly name
+    assert get_base_entity_object_id("", "Device!@#$%") == "device_____"
+
+
+def test_empty_name_no_friendly_name() -> None:
+    """Test when entity has empty name and no friendly name - should use device name."""
+    # Test with CORE.name set
+    CORE.name = "device-name"
+    assert get_base_entity_object_id("", None) == "device-name"
+
+    CORE.name = "Test Device"
+    assert get_base_entity_object_id("", None) == "test_device"
+
+
+def test_edge_cases() -> None:
+    """Test edge cases."""
+    # Only spaces
+    assert get_base_entity_object_id("   ", None) == "___"
+
+    # Unicode characters (should be replaced)
+    assert get_base_entity_object_id("Température", None) == "temp_rature"
+    assert get_base_entity_object_id("测试", None) == "__"
+
+    # Empty string with empty friendly name (empty friendly name is treated as None)
+    # Falls back to CORE.name
+    CORE.name = "device"
+    assert get_base_entity_object_id("", "") == "device"
+
+    # Very long name (should work fine)
+    long_name = "a" * 100 + " " + "b" * 100
+    expected = "a" * 100 + "_" + "b" * 100
+    assert get_base_entity_object_id(long_name, None) == expected
+
+
+@pytest.mark.parametrize(
+    ("name", "expected"),
+    [
+        ("Temperature Sensor", "temperature_sensor"),
+        ("Living Room Light", "living_room_light"),
+        ("Test-Device_123", "test-device_123"),
+        ("Special!@#Chars", "special___chars"),
+        ("UPPERCASE NAME", "uppercase_name"),
+        ("lowercase name", "lowercase_name"),
+        ("Mixed Case Name", "mixed_case_name"),
+        ("   Spaces   ", "___spaces___"),
+    ],
+)
+def test_matches_cpp_helpers(name: str, expected: str) -> None:
+    """Test that the logic matches using snake_case and sanitize directly."""
+    # For non-empty names, verify our function produces same result as direct snake_case + sanitize
+    assert get_base_entity_object_id(name, None) == sanitize(snake_case(name))
+    assert get_base_entity_object_id(name, None) == expected
+
+
+def test_empty_name_fallback() -> None:
+    """Test empty name handling which falls back to friendly_name or CORE.name."""
+    # Empty name is handled specially - it doesn't just use sanitize(snake_case(""))
+    # Instead it falls back to friendly_name or CORE.name
+    assert sanitize(snake_case("")) == ""  # Direct conversion gives empty string
+    # But our function returns a fallback
+    CORE.name = "device"
+    assert get_base_entity_object_id("", None) == "device"  # Uses device name
+
+
+def test_name_add_mac_suffix_behavior() -> None:
+    """Test behavior related to name_add_mac_suffix.
+
+    In C++, when name_add_mac_suffix is enabled and entity has no name,
+    get_object_id() returns str_sanitize(str_snake_case(App.get_friendly_name()))
+    dynamically. Our function always returns the same result since we're
+    calculating the base for duplicate tracking.
+    """
+    # The function should always return the same result regardless of
+    # name_add_mac_suffix setting, as we're calculating the base object_id
+    assert get_base_entity_object_id("", "Test Device") == "test_device"
+    assert get_base_entity_object_id("Entity Name", "Test Device") == "entity_name"
+
+
+def test_priority_order() -> None:
+    """Test the priority order: entity name > device name > friendly name > CORE.name."""
+    CORE.name = "core-device"
+
+    # 1. Entity name has highest priority
+    assert (
+        get_base_entity_object_id("Entity Name", "Friendly Name", "Device Name")
+        == "entity_name"
+    )
+
+    # 2. Device name is next priority (when entity name is empty)
+    assert (
+        get_base_entity_object_id("", "Friendly Name", "Device Name") == "device_name"
+    )
+
+    # 3. Friendly name is next (when entity and device names are empty)
+    assert get_base_entity_object_id("", "Friendly Name", None) == "friendly_name"
+
+    # 4. CORE.name is last resort
+    assert get_base_entity_object_id("", None, None) == "core-device"
+
+
+@pytest.mark.parametrize(
+    ("name", "friendly_name", "device_name", "expected"),
+    [
+        # name, friendly_name, device_name, expected
+        ("Living Room Light", None, None, "living_room_light"),
+        ("", "Kitchen Controller", None, "kitchen_controller"),
+        (
+            "",
+            "ESP32 Device",
+            "controller_1",
+            "controller_1",
+        ),  # Device name takes precedence
+        ("GPIO2 Button", None, None, "gpio2_button"),
+        ("WiFi Signal", "My Device", None, "wifi_signal"),
+        ("", None, "esp32_node", "esp32_node"),
+        ("Front Door Sensor", "Home Assistant", "door_controller", "front_door_sensor"),
+    ],
+)
+def test_real_world_examples(
+    name: str, friendly_name: str | None, device_name: str | None, expected: str
+) -> None:
+    """Test real-world entity naming scenarios."""
+    result = get_base_entity_object_id(name, friendly_name, device_name)
+    assert result == expected
+
+
+def test_issue_6953_scenarios() -> None:
+    """Test specific scenarios from issue #6953."""
+    # Scenario 1: Multiple empty names on main device with name_add_mac_suffix
+    # The Python code calculates the base, C++ might append MAC suffix dynamically
+    CORE.name = "device-name"
+    CORE.friendly_name = "Friendly Device"
+
+    # All empty names should resolve to same base
+    assert get_base_entity_object_id("", CORE.friendly_name) == "friendly_device"
+    assert get_base_entity_object_id("", CORE.friendly_name) == "friendly_device"
+    assert get_base_entity_object_id("", CORE.friendly_name) == "friendly_device"
+
+    # Scenario 2: Empty names on sub-devices
+    assert (
+        get_base_entity_object_id("", "Main Device", "controller_1") == "controller_1"
+    )
+    assert (
+        get_base_entity_object_id("", "Main Device", "controller_2") == "controller_2"
+    )
+
+    # Scenario 3: xyz duplicates
+    assert get_base_entity_object_id("xyz", None) == "xyz"
+    assert get_base_entity_object_id("xyz", "Device") == "xyz"
+
+
+# Tests for setup_entity function
+
+
+@pytest.fixture
+def setup_test_environment() -> Generator[list[str], None, None]:
+    """Set up test environment for setup_entity tests."""
+    # Set CORE state for tests
+    CORE.name = "test-device"
+    CORE.friendly_name = "Test Device"
+    # Store original add function
+
+    original_add = entity_helpers.add
+    # Track what gets added
+    added_expressions: list[str] = []
+
+    def mock_add(expression: Any) -> Any:
+        added_expressions.append(str(expression))
+        return original_add(expression)
+
+    # Patch add function in entity_helpers module
+    entity_helpers.add = mock_add
+    yield added_expressions
+    # Clean up
+    entity_helpers.add = original_add
+
+
+def extract_object_id_from_expressions(expressions: list[str]) -> str | None:
+    """Extract the object ID that was set from the generated expressions."""
+    for expr in expressions:
+        # Look for set_object_id calls with regex to handle various formats
+        # Matches: var.set_object_id("temperature_2") or var.set_object_id('temperature_2')
+        if match := OBJECT_ID_PATTERN.search(expr):
+            return match.group(1)
+    return None
+
+
+@pytest.mark.asyncio
+async def test_setup_entity_no_duplicates(setup_test_environment: list[str]) -> None:
+    """Test setup_entity with unique names."""
+
+    added_expressions = setup_test_environment
+
+    # Create mock entities
+    var1 = MockObj("sensor1")
+    var2 = MockObj("sensor2")
+
+    # Set up first entity
+    config1 = {
+        CONF_NAME: "Temperature",
+        CONF_DISABLED_BY_DEFAULT: False,
+    }
+    await setup_entity(var1, config1, "sensor")
+
+    # Get object ID from first entity
+    object_id1 = extract_object_id_from_expressions(added_expressions)
+    assert object_id1 == "temperature"
+
+    # Clear for next entity
+    added_expressions.clear()
+
+    # Set up second entity with different name
+    config2 = {
+        CONF_NAME: "Humidity",
+        CONF_DISABLED_BY_DEFAULT: False,
+    }
+    await setup_entity(var2, config2, "sensor")
+
+    # Get object ID from second entity
+    object_id2 = extract_object_id_from_expressions(added_expressions)
+    assert object_id2 == "humidity"
+
+
+@pytest.mark.asyncio
+async def test_setup_entity_different_platforms(
+    setup_test_environment: list[str],
+) -> None:
+    """Test that same name on different platforms doesn't conflict."""
+
+    added_expressions = setup_test_environment
+
+    # Create mock entities
+    sensor = MockObj("sensor1")
+    binary_sensor = MockObj("binary_sensor1")
+    text_sensor = MockObj("text_sensor1")
+
+    config = {
+        CONF_NAME: "Status",
+        CONF_DISABLED_BY_DEFAULT: False,
+    }
+
+    # Set up entities on different platforms
+    platforms = [
+        (sensor, "sensor"),
+        (binary_sensor, "binary_sensor"),
+        (text_sensor, "text_sensor"),
+    ]
+
+    object_ids: list[str] = []
+    for var, platform in platforms:
+        added_expressions.clear()
+        await setup_entity(var, config, platform)
+        object_id = extract_object_id_from_expressions(added_expressions)
+        object_ids.append(object_id)
+
+    # All should get base object ID without suffix
+    assert all(obj_id == "status" for obj_id in object_ids)
+
+
+@pytest.fixture
+def mock_get_variable() -> Generator[dict[ID, MockObj], None, None]:
+    """Mock get_variable to return test devices."""
+    devices = {}
+    original_get_variable = entity_helpers.get_variable
+
+    async def _mock_get_variable(device_id: ID) -> MockObj:
+        if device_id in devices:
+            return devices[device_id]
+        return await original_get_variable(device_id)
+
+    entity_helpers.get_variable = _mock_get_variable
+    yield devices
+    # Clean up
+    entity_helpers.get_variable = original_get_variable
+
+
+@pytest.mark.asyncio
+async def test_setup_entity_with_devices(
+    setup_test_environment: list[str], mock_get_variable: dict[ID, MockObj]
+) -> None:
+    """Test that same name on different devices doesn't conflict."""
+    added_expressions = setup_test_environment
+
+    # Create mock devices
+    device1_id = ID("device1", type="Device")
+    device2_id = ID("device2", type="Device")
+    device1 = MockObj("device1_obj")
+    device2 = MockObj("device2_obj")
+
+    # Register devices with the mock
+    mock_get_variable[device1_id] = device1
+    mock_get_variable[device2_id] = device2
+
+    # Create sensors with same name on different devices
+    sensor1 = MockObj("sensor1")
+    sensor2 = MockObj("sensor2")
+
+    config1 = {
+        CONF_NAME: "Temperature",
+        CONF_DEVICE_ID: device1_id,
+        CONF_DISABLED_BY_DEFAULT: False,
+    }
+
+    config2 = {
+        CONF_NAME: "Temperature",
+        CONF_DEVICE_ID: device2_id,
+        CONF_DISABLED_BY_DEFAULT: False,
+    }
+
+    # Get object IDs
+    object_ids: list[str] = []
+    for var, config in [(sensor1, config1), (sensor2, config2)]:
+        added_expressions.clear()
+        await setup_entity(var, config, "sensor")
+        object_id = extract_object_id_from_expressions(added_expressions)
+        object_ids.append(object_id)
+
+    # Both should get base object ID without suffix (different devices)
+    assert object_ids[0] == "temperature"
+    assert object_ids[1] == "temperature"
+
+
+@pytest.mark.asyncio
+async def test_setup_entity_empty_name(setup_test_environment: list[str]) -> None:
+    """Test setup_entity with empty entity name."""
+
+    added_expressions = setup_test_environment
+
+    var = MockObj("sensor1")
+
+    config = {
+        CONF_NAME: "",
+        CONF_DISABLED_BY_DEFAULT: False,
+    }
+
+    await setup_entity(var, config, "sensor")
+
+    object_id = extract_object_id_from_expressions(added_expressions)
+    # Should use friendly name
+    assert object_id == "test_device"
+
+
+@pytest.mark.asyncio
+async def test_setup_entity_special_characters(
+    setup_test_environment: list[str],
+) -> None:
+    """Test setup_entity with names containing special characters."""
+
+    added_expressions = setup_test_environment
+
+    var = MockObj("sensor1")
+
+    config = {
+        CONF_NAME: "Temperature Sensor!",
+        CONF_DISABLED_BY_DEFAULT: False,
+    }
+
+    await setup_entity(var, config, "sensor")
+    object_id = extract_object_id_from_expressions(added_expressions)
+
+    # Special characters should be sanitized
+    assert object_id == "temperature_sensor_"
+
+
+@pytest.mark.asyncio
+async def test_setup_entity_with_icon(setup_test_environment: list[str]) -> None:
+    """Test setup_entity sets icon correctly."""
+
+    added_expressions = setup_test_environment
+
+    var = MockObj("sensor1")
+
+    config = {
+        CONF_NAME: "Temperature",
+        CONF_DISABLED_BY_DEFAULT: False,
+        CONF_ICON: "mdi:thermometer",
+    }
+
+    await setup_entity(var, config, "sensor")
+
+    # Check icon was set
+    assert any(
+        'sensor1.set_icon("mdi:thermometer")' in expr for expr in added_expressions
+    )
+
+
+@pytest.mark.asyncio
+async def test_setup_entity_disabled_by_default(
+    setup_test_environment: list[str],
+) -> None:
+    """Test setup_entity sets disabled_by_default correctly."""
+
+    added_expressions = setup_test_environment
+
+    var = MockObj("sensor1")
+
+    config = {
+        CONF_NAME: "Temperature",
+        CONF_DISABLED_BY_DEFAULT: True,
+    }
+
+    await setup_entity(var, config, "sensor")
+
+    # Check disabled_by_default was set
+    assert any(
+        "sensor1.set_disabled_by_default(true)" in expr for expr in added_expressions
+    )
+
+
+def test_entity_duplicate_validator() -> None:
+    """Test the entity_duplicate_validator function."""
+    from esphome.core.entity_helpers import entity_duplicate_validator
+
+    # Reset CORE unique_ids for clean test
+    CORE.unique_ids.clear()
+
+    # Create validator for sensor platform
+    validator = entity_duplicate_validator("sensor")
+
+    # First entity should pass
+    config1 = {CONF_NAME: "Temperature"}
+    validated1 = validator(config1)
+    assert validated1 == config1
+    assert ("sensor", "temperature") in CORE.unique_ids
+
+    # Second entity with different name should pass
+    config2 = {CONF_NAME: "Humidity"}
+    validated2 = validator(config2)
+    assert validated2 == config2
+    assert ("sensor", "humidity") in CORE.unique_ids
+
+    # Duplicate entity should fail
+    config3 = {CONF_NAME: "Temperature"}
+    with pytest.raises(
+        Invalid, match=r"Duplicate sensor entity with name 'Temperature' found"
+    ):
+        validator(config3)
+
+
+def test_entity_duplicate_validator_with_devices() -> None:
+    """Test entity_duplicate_validator with devices."""
+    from esphome.core.entity_helpers import entity_duplicate_validator
+
+    # Reset CORE unique_ids for clean test
+    CORE.unique_ids.clear()
+
+    # Create validator for sensor platform
+    validator = entity_duplicate_validator("sensor")
+
+    # Create mock device IDs
+    device1 = ID("device1", type="Device")
+    device2 = ID("device2", type="Device")
+
+    # First entity on device1 should pass
+    config1 = {CONF_NAME: "Temperature", CONF_DEVICE_ID: device1}
+    validated1 = validator(config1)
+    assert validated1 == config1
+    assert ("sensor", "temperature") in CORE.unique_ids
+
+    # Same name on different device should now fail
+    config2 = {CONF_NAME: "Temperature", CONF_DEVICE_ID: device2}
+    with pytest.raises(
+        Invalid,
+        match=r"Duplicate sensor entity with name 'Temperature' found. Each entity must have a unique name within its platform across all devices.",
+    ):
+        validator(config2)
+
+    # Different name on device2 should pass
+    config3 = {CONF_NAME: "Humidity", CONF_DEVICE_ID: device2}
+    validated3 = validator(config3)
+    assert validated3 == config3
+    assert ("sensor", "humidity") in CORE.unique_ids
+
+    # Empty names should use device names and be allowed
+    config4 = {CONF_NAME: "", CONF_DEVICE_ID: device1}
+    validated4 = validator(config4)
+    assert validated4 == config4
+    assert ("sensor", "device1") in CORE.unique_ids
+
+    config5 = {CONF_NAME: "", CONF_DEVICE_ID: device2}
+    validated5 = validator(config5)
+    assert validated5 == config5
+    assert ("sensor", "device2") in CORE.unique_ids
+
+
+def test_duplicate_entity_yaml_validation(
+    yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str]
+) -> None:
+    """Test that duplicate entity names are caught during YAML config validation."""
+    result = load_config_from_fixture(yaml_file, "duplicate_entity.yaml", FIXTURES_DIR)
+    assert result is None
+
+    # Check for the duplicate entity error message
+    captured = capsys.readouterr()
+    assert "Duplicate sensor entity with name 'Temperature' found" in captured.out
+
+
+def test_duplicate_entity_with_devices_yaml_validation(
+    yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str]
+) -> None:
+    """Test duplicate entity validation with devices."""
+    result = load_config_from_fixture(
+        yaml_file, "duplicate_entity_with_devices.yaml", FIXTURES_DIR
+    )
+    assert result is None
+
+    # Check for the duplicate entity error message
+    captured = capsys.readouterr()
+    assert (
+        "Duplicate sensor entity with name 'Temperature' found. Each entity must have a unique name within its platform across all devices."
+        in captured.out
+    )
+
+
+def test_entity_different_platforms_yaml_validation(
+    yaml_file: Callable[[str], str],
+) -> None:
+    """Test that same entity name on different platforms is allowed."""
+    result = load_config_from_fixture(
+        yaml_file, "entity_different_platforms.yaml", FIXTURES_DIR
+    )
+    # This should succeed
+    assert result is not None
diff --git a/tests/unit_tests/fixtures/core/config/area_id_collision.yaml b/tests/unit_tests/fixtures/core/config/area_id_collision.yaml
new file mode 100644
index 0000000000..fb2e930e61
--- /dev/null
+++ b/tests/unit_tests/fixtures/core/config/area_id_collision.yaml
@@ -0,0 +1,10 @@
+esphome:
+  name: test-collision
+  area:
+    id: duplicate_id
+    name: Area 1
+  areas:
+    - id: duplicate_id
+      name: Area 2
+
+host:
diff --git a/tests/unit_tests/fixtures/core/config/area_id_hash_collision.yaml b/tests/unit_tests/fixtures/core/config/area_id_hash_collision.yaml
new file mode 100644
index 0000000000..3a2e8ab8a9
--- /dev/null
+++ b/tests/unit_tests/fixtures/core/config/area_id_hash_collision.yaml
@@ -0,0 +1,10 @@
+esphome:
+  name: test
+  areas:
+    - id: test_2258
+      name: "Area 1"
+    - id: d6ka
+      name: "Area 2"
+
+esp32:
+  board: esp32dev
diff --git a/tests/unit_tests/fixtures/core/config/device_duplicate_id.yaml b/tests/unit_tests/fixtures/core/config/device_duplicate_id.yaml
new file mode 100644
index 0000000000..2aa3055686
--- /dev/null
+++ b/tests/unit_tests/fixtures/core/config/device_duplicate_id.yaml
@@ -0,0 +1,10 @@
+esphome:
+  name: test
+  devices:
+    - id: duplicate_device
+      name: "Device 1"
+    - id: duplicate_device
+      name: "Device 2"
+
+esp32:
+  board: esp32dev
diff --git a/tests/unit_tests/fixtures/core/config/device_id_collision.yaml b/tests/unit_tests/fixtures/core/config/device_id_collision.yaml
new file mode 100644
index 0000000000..9cf04e0595
--- /dev/null
+++ b/tests/unit_tests/fixtures/core/config/device_id_collision.yaml
@@ -0,0 +1,10 @@
+esphome:
+  name: test
+  devices:
+    - id: test_2258
+      name: "Device 1"
+    - id: d6ka
+      name: "Device 2"
+
+esp32:
+  board: esp32dev
diff --git a/tests/unit_tests/fixtures/core/config/device_invalid_area.yaml b/tests/unit_tests/fixtures/core/config/device_invalid_area.yaml
new file mode 100644
index 0000000000..9a8ec0a1eb
--- /dev/null
+++ b/tests/unit_tests/fixtures/core/config/device_invalid_area.yaml
@@ -0,0 +1,12 @@
+esphome:
+  name: test
+  areas:
+    - id: valid_area
+      name: "Valid Area"
+  devices:
+    - id: test_device
+      name: "Test Device"
+      area_id: nonexistent_area
+
+esp32:
+  board: esp32dev
diff --git a/tests/unit_tests/fixtures/core/config/device_without_area.yaml b/tests/unit_tests/fixtures/core/config/device_without_area.yaml
new file mode 100644
index 0000000000..8464cf37df
--- /dev/null
+++ b/tests/unit_tests/fixtures/core/config/device_without_area.yaml
@@ -0,0 +1,7 @@
+esphome:
+  name: test-device-no-area
+  devices:
+    - id: test_device
+      name: Test Device
+
+host:
diff --git a/tests/unit_tests/fixtures/core/config/legacy_string_area.yaml b/tests/unit_tests/fixtures/core/config/legacy_string_area.yaml
new file mode 100644
index 0000000000..fe2dc3db17
--- /dev/null
+++ b/tests/unit_tests/fixtures/core/config/legacy_string_area.yaml
@@ -0,0 +1,5 @@
+esphome:
+  name: test-legacy-area
+  area: Living Room
+
+host:
diff --git a/tests/unit_tests/fixtures/core/config/multiple_areas_devices.yaml b/tests/unit_tests/fixtures/core/config/multiple_areas_devices.yaml
new file mode 100644
index 0000000000..ef3b4f6e67
--- /dev/null
+++ b/tests/unit_tests/fixtures/core/config/multiple_areas_devices.yaml
@@ -0,0 +1,22 @@
+esphome:
+  name: test-multiple
+  area:
+    id: main_area
+    name: Main Area
+  areas:
+    - id: area1
+      name: Area 1
+    - id: area2
+      name: Area 2
+  devices:
+    - id: device1
+      name: Device 1
+      area_id: main_area
+    - id: device2
+      name: Device 2
+      area_id: area1
+    - id: device3
+      name: Device 3
+      area_id: area2
+
+host:
diff --git a/tests/unit_tests/fixtures/core/config/valid_area_device.yaml b/tests/unit_tests/fixtures/core/config/valid_area_device.yaml
new file mode 100644
index 0000000000..fc97894586
--- /dev/null
+++ b/tests/unit_tests/fixtures/core/config/valid_area_device.yaml
@@ -0,0 +1,11 @@
+esphome:
+  name: test-valid-area
+  areas:
+    - id: bedroom_area
+      name: Bedroom
+  devices:
+    - id: test_device
+      name: Test Device
+      area_id: bedroom_area
+
+host:
diff --git a/tests/unit_tests/fixtures/core/entity_helpers/duplicate_entity.yaml b/tests/unit_tests/fixtures/core/entity_helpers/duplicate_entity.yaml
new file mode 100644
index 0000000000..2a8dad66c9
--- /dev/null
+++ b/tests/unit_tests/fixtures/core/entity_helpers/duplicate_entity.yaml
@@ -0,0 +1,13 @@
+esphome:
+  name: test-duplicate
+
+esp32:
+  board: esp32dev
+
+sensor:
+  - platform: template
+    name: "Temperature"
+    lambda: return 21.0;
+  - platform: template
+    name: "Temperature"  # Duplicate - should fail
+    lambda: return 22.0;
diff --git a/tests/unit_tests/fixtures/core/entity_helpers/duplicate_entity_with_devices.yaml b/tests/unit_tests/fixtures/core/entity_helpers/duplicate_entity_with_devices.yaml
new file mode 100644
index 0000000000..42e16231a5
--- /dev/null
+++ b/tests/unit_tests/fixtures/core/entity_helpers/duplicate_entity_with_devices.yaml
@@ -0,0 +1,26 @@
+esphome:
+  name: test-duplicate-devices
+  devices:
+    - id: device1
+      name: "Device 1"
+    - id: device2
+      name: "Device 2"
+
+esp32:
+  board: esp32dev
+
+sensor:
+  # Same name on different devices - should pass
+  - platform: template
+    device_id: device1
+    name: "Temperature"
+    lambda: return 21.0;
+  - platform: template
+    device_id: device2
+    name: "Temperature"
+    lambda: return 22.0;
+  # Duplicate on same device - should fail
+  - platform: template
+    device_id: device1
+    name: "Temperature"
+    lambda: return 23.0;
diff --git a/tests/unit_tests/fixtures/core/entity_helpers/entity_different_platforms.yaml b/tests/unit_tests/fixtures/core/entity_helpers/entity_different_platforms.yaml
new file mode 100644
index 0000000000..00181c52c4
--- /dev/null
+++ b/tests/unit_tests/fixtures/core/entity_helpers/entity_different_platforms.yaml
@@ -0,0 +1,20 @@
+esphome:
+  name: test-different-platforms
+
+esp32:
+  board: esp32dev
+
+sensor:
+  - platform: template
+    name: "Status"
+    lambda: return 1.0;
+
+binary_sensor:
+  - platform: template
+    name: "Status"  # Same name, different platform - should pass
+    lambda: return true;
+
+text_sensor:
+  - platform: template
+    name: "Status"  # Same name, different platform - should pass
+    lambda: return {"OK"};
diff --git a/tests/unit_tests/fixtures/substitutions/.gitignore b/tests/unit_tests/fixtures/substitutions/.gitignore
new file mode 100644
index 0000000000..0b15cdb2b7
--- /dev/null
+++ b/tests/unit_tests/fixtures/substitutions/.gitignore
@@ -0,0 +1 @@
+*.received.yaml
\ No newline at end of file
diff --git a/tests/unit_tests/fixtures/substitutions/00-simple_var.approved.yaml b/tests/unit_tests/fixtures/substitutions/00-simple_var.approved.yaml
new file mode 100644
index 0000000000..c031399c37
--- /dev/null
+++ b/tests/unit_tests/fixtures/substitutions/00-simple_var.approved.yaml
@@ -0,0 +1,19 @@
+substitutions:
+  var1: '1'
+  var2: '2'
+  var21: '79'
+esphome:
+  name: test
+test_list:
+  - '1'
+  - '1'
+  - '1'
+  - '1'
+  - 'Values: 1 2'
+  - 'Value: 79'
+  - 1 + 2
+  - 1 * 2
+  - 'Undefined var: ${undefined_var}'
+  - ${undefined_var}
+  - $undefined_var
+  - ${ undefined_var }
diff --git a/tests/unit_tests/fixtures/substitutions/00-simple_var.input.yaml b/tests/unit_tests/fixtures/substitutions/00-simple_var.input.yaml
new file mode 100644
index 0000000000..88a4ffb991
--- /dev/null
+++ b/tests/unit_tests/fixtures/substitutions/00-simple_var.input.yaml
@@ -0,0 +1,21 @@
+esphome:
+  name: test
+
+substitutions:
+  var1: "1"
+  var2: "2"
+  var21: "79"
+
+test_list:
+  - "$var1"
+  - "${var1}"
+  - $var1
+  - ${var1}
+  - "Values: $var1 ${var2}"
+  - "Value: ${var2${var1}}"
+  - "$var1 + $var2"
+  - "${ var1 } * ${ var2 }"
+  - "Undefined var: ${undefined_var}"
+  - ${undefined_var}
+  - $undefined_var
+  - ${ undefined_var }
diff --git a/tests/unit_tests/fixtures/substitutions/01-include.approved.yaml b/tests/unit_tests/fixtures/substitutions/01-include.approved.yaml
new file mode 100644
index 0000000000..a812fedcfd
--- /dev/null
+++ b/tests/unit_tests/fixtures/substitutions/01-include.approved.yaml
@@ -0,0 +1,15 @@
+substitutions:
+  var1: '1'
+  var2: '2'
+  a: alpha
+test_list:
+  - values:
+      - var1: '1'
+      - a: A
+      - b: B-default
+      - c: The value of C is C
+  - values:
+      - var1: '1'
+      - a: alpha
+      - b: beta
+      - c: The value of C is $c
diff --git a/tests/unit_tests/fixtures/substitutions/01-include.input.yaml b/tests/unit_tests/fixtures/substitutions/01-include.input.yaml
new file mode 100644
index 0000000000..d3daa681a4
--- /dev/null
+++ b/tests/unit_tests/fixtures/substitutions/01-include.input.yaml
@@ -0,0 +1,15 @@
+substitutions:
+  var1: "1"
+  var2: "2"
+  a: "alpha"
+
+test_list:
+  - !include
+    file: inc1.yaml
+    vars:
+      a: "A"
+      c: "C"
+  - !include
+    file: inc1.yaml
+    vars:
+      b: "beta"
diff --git a/tests/unit_tests/fixtures/substitutions/02-expressions.approved.yaml b/tests/unit_tests/fixtures/substitutions/02-expressions.approved.yaml
new file mode 100644
index 0000000000..9e401ec5d6
--- /dev/null
+++ b/tests/unit_tests/fixtures/substitutions/02-expressions.approved.yaml
@@ -0,0 +1,24 @@
+substitutions:
+  width: 7
+  height: 8
+  enabled: true
+  pin: &id001
+    number: 18
+    inverted: true
+  area: 25
+  numberOne: 1
+  var1: 79
+test_list:
+  - The area is 56
+  - 56
+  - 56 + 1
+  - ENABLED
+  - list:
+      - 7
+      - 8
+  - width: 7
+    height: 8
+  - *id001
+  - The pin number is 18
+  - The square root is: 5.0
+  - The number is 80
diff --git a/tests/unit_tests/fixtures/substitutions/02-expressions.input.yaml b/tests/unit_tests/fixtures/substitutions/02-expressions.input.yaml
new file mode 100644
index 0000000000..1777b46f67
--- /dev/null
+++ b/tests/unit_tests/fixtures/substitutions/02-expressions.input.yaml
@@ -0,0 +1,22 @@
+substitutions:
+  width: 7
+  height: 8
+  enabled: true
+  pin:
+    number: 18
+    inverted: true
+  area: 25
+  numberOne: 1
+  var1: 79
+
+test_list:
+  - "The area is ${width * height}"
+  - ${width * height}
+  - ${width * height} + 1
+  - ${enabled and "ENABLED" or "DISABLED"}
+  - list: ${ [width, height] }
+  - "${ {'width': width, 'height': height} }"
+  - ${pin}
+  - The pin number is ${pin.number}
+  - The square root is: ${math.sqrt(area)}
+  - The number is ${var${numberOne} + 1}
diff --git a/tests/unit_tests/fixtures/substitutions/03-closures.approved.yaml b/tests/unit_tests/fixtures/substitutions/03-closures.approved.yaml
new file mode 100644
index 0000000000..c8f7d9976c
--- /dev/null
+++ b/tests/unit_tests/fixtures/substitutions/03-closures.approved.yaml
@@ -0,0 +1,17 @@
+substitutions:
+  B: 5
+  var7: 79
+package_result:
+  - The value of A*B is 35, where A is a package var and B is a substitution in the
+    root file
+  - Double substitution also works; the value of var7 is 79, where A is a package
+    var
+local_results:
+  - The value of B is 5
+  - 'You will see, however, that
+
+    ${A} is not substituted here, since
+
+    it is out of scope.
+
+    '
diff --git a/tests/unit_tests/fixtures/substitutions/03-closures.input.yaml b/tests/unit_tests/fixtures/substitutions/03-closures.input.yaml
new file mode 100644
index 0000000000..e0b2c39e52
--- /dev/null
+++ b/tests/unit_tests/fixtures/substitutions/03-closures.input.yaml
@@ -0,0 +1,16 @@
+substitutions:
+  B: 5
+  var7: 79
+
+packages:
+  closures_package: !include
+    file: closures_package.yaml
+    vars:
+      A: 7
+
+local_results:
+  - The value of B is ${B}
+  - |
+    You will see, however, that
+    ${A} is not substituted here, since
+    it is out of scope.
diff --git a/tests/unit_tests/fixtures/substitutions/04-display_example.approved.yaml b/tests/unit_tests/fixtures/substitutions/04-display_example.approved.yaml
new file mode 100644
index 0000000000..f559181b45
--- /dev/null
+++ b/tests/unit_tests/fixtures/substitutions/04-display_example.approved.yaml
@@ -0,0 +1,5 @@
+display:
+  - platform: ili9xxx
+    dimensions:
+      width: 960
+      height: 544
diff --git a/tests/unit_tests/fixtures/substitutions/04-display_example.input.yaml b/tests/unit_tests/fixtures/substitutions/04-display_example.input.yaml
new file mode 100644
index 0000000000..9d8f64a253
--- /dev/null
+++ b/tests/unit_tests/fixtures/substitutions/04-display_example.input.yaml
@@ -0,0 +1,7 @@
+# main.yaml
+packages:
+  my_display: !include
+    file: display.yaml
+    vars:
+      high_dpi: true
+      native_height: 272
diff --git a/tests/unit_tests/fixtures/substitutions/closures_package.yaml b/tests/unit_tests/fixtures/substitutions/closures_package.yaml
new file mode 100644
index 0000000000..e87908814d
--- /dev/null
+++ b/tests/unit_tests/fixtures/substitutions/closures_package.yaml
@@ -0,0 +1,3 @@
+package_result:
+  - The value of A*B is ${A * B}, where A is a package var and B is a substitution in the root file
+  - Double substitution also works; the value of var7 is ${var$A}, where A is a package var
diff --git a/tests/unit_tests/fixtures/substitutions/display.yaml b/tests/unit_tests/fixtures/substitutions/display.yaml
new file mode 100644
index 0000000000..1e2249dddb
--- /dev/null
+++ b/tests/unit_tests/fixtures/substitutions/display.yaml
@@ -0,0 +1,11 @@
+# display.yaml
+
+defaults:
+  native_width: 480
+  native_height: 480
+
+display:
+  - platform: ili9xxx
+    dimensions:
+      width: ${high_dpi and native_width * 2 or native_width}
+      height: ${high_dpi and native_height * 2 or native_height}
diff --git a/tests/unit_tests/fixtures/substitutions/inc1.yaml b/tests/unit_tests/fixtures/substitutions/inc1.yaml
new file mode 100644
index 0000000000..65b91a5e16
--- /dev/null
+++ b/tests/unit_tests/fixtures/substitutions/inc1.yaml
@@ -0,0 +1,8 @@
+defaults:
+  b: "B-default"
+
+values:
+  - var1: $var1
+  - a: $a
+  - b: ${b}
+  - c: The value of C is $c
diff --git a/tests/unit_tests/test_config_helpers.py b/tests/unit_tests/test_config_helpers.py
new file mode 100644
index 0000000000..1c850e3759
--- /dev/null
+++ b/tests/unit_tests/test_config_helpers.py
@@ -0,0 +1,135 @@
+"""Unit tests for esphome.config_helpers module."""
+
+from collections.abc import Callable
+from unittest.mock import patch
+
+from esphome.config_helpers import filter_source_files_from_platform, get_logger_level
+from esphome.const import (
+    CONF_LEVEL,
+    CONF_LOGGER,
+    KEY_CORE,
+    KEY_TARGET_FRAMEWORK,
+    KEY_TARGET_PLATFORM,
+    PlatformFramework,
+)
+
+
+def test_filter_source_files_from_platform_esp32() -> None:
+    """Test that filter_source_files_from_platform correctly filters files for ESP32 platform."""
+    # Define test file mappings
+    files_map: dict[str, set[PlatformFramework]] = {
+        "logger_esp32.cpp": {
+            PlatformFramework.ESP32_ARDUINO,
+            PlatformFramework.ESP32_IDF,
+        },
+        "logger_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
+        "logger_host.cpp": {PlatformFramework.HOST_NATIVE},
+        "logger_common.cpp": {
+            PlatformFramework.ESP32_ARDUINO,
+            PlatformFramework.ESP32_IDF,
+            PlatformFramework.ESP8266_ARDUINO,
+            PlatformFramework.HOST_NATIVE,
+        },
+    }
+
+    # Create the filter function
+    filter_func: Callable[[], list[str]] = filter_source_files_from_platform(files_map)
+
+    # Test ESP32 with Arduino framework
+    mock_core_data: dict[str, dict[str, str]] = {
+        KEY_CORE: {
+            KEY_TARGET_PLATFORM: "esp32",
+            KEY_TARGET_FRAMEWORK: "arduino",
+        }
+    }
+
+    with patch("esphome.config_helpers.CORE.data", mock_core_data):
+        excluded: list[str] = filter_func()
+        # ESP32 Arduino should exclude ESP8266 and HOST files
+        assert "logger_esp8266.cpp" in excluded
+        assert "logger_host.cpp" in excluded
+        # But not ESP32 or common files
+        assert "logger_esp32.cpp" not in excluded
+        assert "logger_common.cpp" not in excluded
+
+
+def test_filter_source_files_from_platform_host() -> None:
+    """Test that filter_source_files_from_platform correctly filters files for HOST platform."""
+    # Define test file mappings
+    files_map: dict[str, set[PlatformFramework]] = {
+        "logger_esp32.cpp": {
+            PlatformFramework.ESP32_ARDUINO,
+            PlatformFramework.ESP32_IDF,
+        },
+        "logger_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
+        "logger_host.cpp": {PlatformFramework.HOST_NATIVE},
+        "logger_common.cpp": {
+            PlatformFramework.ESP32_ARDUINO,
+            PlatformFramework.ESP32_IDF,
+            PlatformFramework.ESP8266_ARDUINO,
+            PlatformFramework.HOST_NATIVE,
+        },
+    }
+
+    # Create the filter function
+    filter_func: Callable[[], list[str]] = filter_source_files_from_platform(files_map)
+
+    # Test Host platform
+    mock_core_data: dict[str, dict[str, str]] = {
+        KEY_CORE: {
+            KEY_TARGET_PLATFORM: "host",
+            KEY_TARGET_FRAMEWORK: "host",  # Framework.NATIVE is "host"
+        }
+    }
+
+    with patch("esphome.config_helpers.CORE.data", mock_core_data):
+        excluded: list[str] = filter_func()
+        # Host should exclude ESP32 and ESP8266 files
+        assert "logger_esp32.cpp" in excluded
+        assert "logger_esp8266.cpp" in excluded
+        # But not host or common files
+        assert "logger_host.cpp" not in excluded
+        assert "logger_common.cpp" not in excluded
+
+
+def test_filter_source_files_from_platform_handles_missing_data() -> None:
+    """Test that filter_source_files_from_platform returns empty list when platform/framework data is missing."""
+    # Define test file mappings
+    files_map: dict[str, set[PlatformFramework]] = {
+        "logger_esp32.cpp": {PlatformFramework.ESP32_ARDUINO},
+        "logger_host.cpp": {PlatformFramework.HOST_NATIVE},
+    }
+
+    # Create the filter function
+    filter_func: Callable[[], list[str]] = filter_source_files_from_platform(files_map)
+
+    # Test case: Missing platform/framework data
+    mock_core_data: dict[str, dict[str, str]] = {KEY_CORE: {}}
+
+    with patch("esphome.config_helpers.CORE.data", mock_core_data):
+        excluded: list[str] = filter_func()
+        # Should return empty list when platform/framework not set
+        assert excluded == []
+
+
+def test_get_logger_level() -> None:
+    """Test get_logger_level helper function."""
+    # Test no logger config - should return default DEBUG
+    mock_config = {}
+    with patch("esphome.config_helpers.CORE.config", mock_config):
+        assert get_logger_level() == "DEBUG"
+
+    # Test with logger set to INFO
+    mock_config = {CONF_LOGGER: {CONF_LEVEL: "INFO"}}
+    with patch("esphome.config_helpers.CORE.config", mock_config):
+        assert get_logger_level() == "INFO"
+
+    # Test with VERY_VERBOSE
+    mock_config = {CONF_LOGGER: {CONF_LEVEL: "VERY_VERBOSE"}}
+    with patch("esphome.config_helpers.CORE.config", mock_config):
+        assert get_logger_level() == "VERY_VERBOSE"
+
+    # Test with logger missing level (uses default DEBUG)
+    mock_config = {CONF_LOGGER: {}}
+    with patch("esphome.config_helpers.CORE.config", mock_config):
+        assert get_logger_level() == "DEBUG"
diff --git a/tests/unit_tests/test_config_validation.py b/tests/unit_tests/test_config_validation.py
index 7a1354589c..2928c5c83a 100644
--- a/tests/unit_tests/test_config_validation.py
+++ b/tests/unit_tests/test_config_validation.py
@@ -20,6 +20,7 @@ from esphome.const import (
     PLATFORM_ESP32,
     PLATFORM_ESP8266,
     PLATFORM_HOST,
+    PLATFORM_LN882X,
     PLATFORM_RP2040,
     PLATFORM_RTL87XX,
 )
@@ -214,7 +215,8 @@ def hex_int__valid(value):
         ("arduino", PLATFORM_RP2040, None, "20", "20", "20", "20"),
         ("arduino", PLATFORM_BK72XX, None, "21", "21", "21", "21"),
         ("arduino", PLATFORM_RTL87XX, None, "22", "22", "22", "22"),
-        ("host", PLATFORM_HOST, None, "23", "23", "23", "23"),
+        ("arduino", PLATFORM_LN882X, None, "23", "23", "23", "23"),
+        ("host", PLATFORM_HOST, None, "24", "24", "24", "24"),
     ],
 )
 def test_split_default(framework, platform, variant, full, idf, arduino, simple):
@@ -244,7 +246,8 @@ def test_split_default(framework, platform, variant, full, idf, arduino, simple)
         "rp2040": "20",
         "bk72xx": "21",
         "rtl87xx": "22",
-        "host": "23",
+        "ln882x": "23",
+        "host": "24",
     }
 
     idf_mappings = {
diff --git a/tests/unit_tests/test_loader.py b/tests/unit_tests/test_loader.py
new file mode 100644
index 0000000000..c6d4c4aef0
--- /dev/null
+++ b/tests/unit_tests/test_loader.py
@@ -0,0 +1,63 @@
+"""Unit tests for esphome.loader module."""
+
+from unittest.mock import MagicMock, patch
+
+from esphome.loader import ComponentManifest
+
+
+def test_component_manifest_resources_with_filter_source_files() -> None:
+    """Test that ComponentManifest.resources correctly filters out excluded files."""
+    # Create a mock module with FILTER_SOURCE_FILES function
+    mock_module = MagicMock()
+    mock_module.FILTER_SOURCE_FILES = lambda: [
+        "platform_esp32.cpp",
+        "platform_esp8266.cpp",
+    ]
+    mock_module.__package__ = "esphome.components.test_component"
+
+    # Create ComponentManifest instance
+    manifest = ComponentManifest(mock_module)
+
+    # Mock the files in the package
+    def create_mock_file(filename: str) -> MagicMock:
+        mock_file = MagicMock()
+        mock_file.name = filename
+        mock_file.is_file.return_value = True
+        return mock_file
+
+    mock_files = [
+        create_mock_file("test.cpp"),
+        create_mock_file("test.h"),
+        create_mock_file("platform_esp32.cpp"),
+        create_mock_file("platform_esp8266.cpp"),
+        create_mock_file("common.cpp"),
+        create_mock_file("README.md"),  # Should be excluded by extension
+    ]
+
+    # Mock importlib.resources
+    with patch("importlib.resources.files") as mock_files_func:
+        mock_package_files = MagicMock()
+        mock_package_files.iterdir.return_value = mock_files
+        mock_package_files.joinpath = lambda name: MagicMock(is_file=lambda: True)
+        mock_files_func.return_value = mock_package_files
+
+        # Get resources
+        resources = manifest.resources
+
+        # Convert to list of filenames for easier testing
+        resource_names = [r.resource for r in resources]
+
+        # Check that platform files are excluded
+        assert "platform_esp32.cpp" not in resource_names
+        assert "platform_esp8266.cpp" not in resource_names
+
+        # Check that other source files are included
+        assert "test.cpp" in resource_names
+        assert "test.h" in resource_names
+        assert "common.cpp" in resource_names
+
+        # Check that non-source files are excluded
+        assert "README.md" not in resource_names
+
+        # Verify the correct number of resources
+        assert len(resources) == 3  # test.cpp, test.h, common.cpp
diff --git a/tests/unit_tests/test_substitutions.py b/tests/unit_tests/test_substitutions.py
new file mode 100644
index 0000000000..b377499d29
--- /dev/null
+++ b/tests/unit_tests/test_substitutions.py
@@ -0,0 +1,125 @@
+import glob
+import logging
+import os
+
+from esphome import yaml_util
+from esphome.components import substitutions
+from esphome.const import CONF_PACKAGES
+
+_LOGGER = logging.getLogger(__name__)
+
+# Set to True for dev mode behavior
+# This will generate the expected version of the test files.
+
+DEV_MODE = False
+
+
+def sort_dicts(obj):
+    """Recursively sort dictionaries for order-insensitive comparison."""
+    if isinstance(obj, dict):
+        return {k: sort_dicts(obj[k]) for k in sorted(obj)}
+    elif isinstance(obj, list):
+        # Lists are not sorted; we preserve order
+        return [sort_dicts(i) for i in obj]
+    else:
+        return obj
+
+
+def dict_diff(a, b, path=""):
+    """Recursively find differences between two dict/list structures."""
+    diffs = []
+    if isinstance(a, dict) and isinstance(b, dict):
+        a_keys = set(a)
+        b_keys = set(b)
+        for key in a_keys - b_keys:
+            diffs.append(f"{path}/{key} only in actual")
+        for key in b_keys - a_keys:
+            diffs.append(f"{path}/{key} only in expected")
+        for key in a_keys & b_keys:
+            diffs.extend(dict_diff(a[key], b[key], f"{path}/{key}"))
+    elif isinstance(a, list) and isinstance(b, list):
+        min_len = min(len(a), len(b))
+        for i in range(min_len):
+            diffs.extend(dict_diff(a[i], b[i], f"{path}[{i}]"))
+        if len(a) > len(b):
+            for i in range(min_len, len(a)):
+                diffs.append(f"{path}[{i}] only in actual: {a[i]!r}")
+        elif len(b) > len(a):
+            for i in range(min_len, len(b)):
+                diffs.append(f"{path}[{i}] only in expected: {b[i]!r}")
+    else:
+        if a != b:
+            diffs.append(f"\t{path}: actual={a!r} expected={b!r}")
+    return diffs
+
+
+def write_yaml(path, data):
+    with open(path, "w", encoding="utf-8") as f:
+        f.write(yaml_util.dump(data))
+
+
+def test_substitutions_fixtures(fixture_path):
+    base_dir = fixture_path / "substitutions"
+    sources = sorted(glob.glob(str(base_dir / "*.input.yaml")))
+    assert sources, f"No input YAML files found in {base_dir}"
+
+    failures = []
+    for source_path in sources:
+        try:
+            expected_path = source_path.replace(".input.yaml", ".approved.yaml")
+            test_case = os.path.splitext(os.path.basename(source_path))[0].replace(
+                ".input", ""
+            )
+
+            # Load using ESPHome's YAML loader
+            config = yaml_util.load_yaml(source_path)
+
+            if CONF_PACKAGES in config:
+                from esphome.components.packages import do_packages_pass
+
+                config = do_packages_pass(config)
+
+            substitutions.do_substitution_pass(config, None)
+
+            # Also load expected using ESPHome's loader, or use {} if missing and DEV_MODE
+            if os.path.isfile(expected_path):
+                expected = yaml_util.load_yaml(expected_path)
+            elif DEV_MODE:
+                expected = {}
+            else:
+                assert os.path.isfile(expected_path), (
+                    f"Expected file missing: {expected_path}"
+                )
+
+            # Sort dicts only (not lists) for comparison
+            got_sorted = sort_dicts(config)
+            expected_sorted = sort_dicts(expected)
+
+            if got_sorted != expected_sorted:
+                diff = "\n".join(dict_diff(got_sorted, expected_sorted))
+                msg = (
+                    f"Substitution result mismatch for {os.path.basename(source_path)}\n"
+                    f"Diff:\n{diff}\n\n"
+                    f"Got:      {got_sorted}\n"
+                    f"Expected: {expected_sorted}"
+                )
+                # Write out the received file when test fails
+                if DEV_MODE:
+                    received_path = os.path.join(
+                        os.path.dirname(source_path), f"{test_case}.received.yaml"
+                    )
+                    write_yaml(received_path, config)
+                    print(msg)
+                    failures.append(msg)
+                else:
+                    raise AssertionError(msg)
+        except Exception as err:
+            _LOGGER.error("Error in test file %s", source_path)
+            raise err
+
+    if DEV_MODE and failures:
+        print(f"\n{len(failures)} substitution test case(s) failed.")
+
+    if DEV_MODE:
+        _LOGGER.error("Tests passed, but Dev mode is enabled.")
+    assert not DEV_MODE  # make sure DEV_MODE is disabled after you are finished.
diff --git a/tests/unit_tests/test_wizard.py b/tests/unit_tests/test_wizard.py
index 6d360740f4..ab20b2abb5 100644
--- a/tests/unit_tests/test_wizard.py
+++ b/tests/unit_tests/test_wizard.py
@@ -8,6 +8,7 @@ import pytest
 from esphome.components.bk72xx.boards import BK72XX_BOARD_PINS
 from esphome.components.esp32.boards import ESP32_BOARD_PINS
 from esphome.components.esp8266.boards import ESP8266_BOARD_PINS
+from esphome.components.ln882x.boards import LN882X_BOARD_PINS
 from esphome.components.rtl87xx.boards import RTL87XX_BOARD_PINS
 from esphome.core import CORE
 import esphome.wizard as wz
@@ -187,6 +188,27 @@ def test_wizard_write_defaults_platform_from_board_bk72xx(
     assert "bk72xx:" in generated_config
 
 
+def test_wizard_write_defaults_platform_from_board_ln882x(
+    default_config, tmp_path, monkeypatch
+):
+    """
+    If the platform is not explicitly set, use "LN882X" if the board is one of LN882X boards
+    """
+    # Given
+    del default_config["platform"]
+    default_config["board"] = [*LN882X_BOARD_PINS][0]
+
+    monkeypatch.setattr(wz, "write_file", MagicMock())
+    monkeypatch.setattr(CORE, "config_path", os.path.dirname(tmp_path))
+
+    # When
+    wz.wizard_write(tmp_path, **default_config)
+
+    # Then
+    generated_config = wz.write_file.call_args.args[1]
+    assert "ln882x:" in generated_config
+
+
 def test_wizard_write_defaults_platform_from_board_rtl87xx(
     default_config, tmp_path, monkeypatch
 ):