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/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 66ea80f8d6..ca3849eb0d 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 @@ -247,6 +250,7 @@ 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 @@ -323,6 +327,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 +335,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 +442,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 @@ -490,10 +498,11 @@ esphome/components/vbus/* @ssieb esphome/components/veml3235/* @kbx81 esphome/components/veml7700/* @latonita esphome/components/version/* @esphome/core -esphome/components/voice_assistant/* @jesserockz +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 +529,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/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/adc_sensor.h b/esphome/components/adc/adc_sensor.h index da02c2d541..46caa55e33 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 { @@ -77,9 +81,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 @@ -91,11 +95,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/api/__init__.py b/esphome/components/api/__init__.py index bd131ef8de..2f1be28293 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -110,9 +110,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 +132,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 +165,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 +184,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") diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 84dc465503..0fa02299ef 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 { @@ -266,6 +283,7 @@ enum EntityCategory { // ==================== BINARY SENSOR ==================== message ListEntitiesBinarySensorResponse { option (id) = 12; + option (base_class) = "InfoResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_BINARY_SENSOR"; @@ -278,9 +296,11 @@ message ListEntitiesBinarySensorResponse { bool disabled_by_default = 7; string icon = 8; EntityCategory entity_category = 9; + uint32 device_id = 10; } message BinarySensorStateResponse { option (id) = 21; + option (base_class) = "StateResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_BINARY_SENSOR"; option (no_delay) = true; @@ -290,11 +310,13 @@ 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 ==================== message ListEntitiesCoverResponse { option (id) = 13; + option (base_class) = "InfoResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_COVER"; @@ -310,6 +332,7 @@ message ListEntitiesCoverResponse { string icon = 10; EntityCategory entity_category = 11; bool supports_stop = 12; + uint32 device_id = 13; } enum LegacyCoverState { @@ -323,6 +346,7 @@ enum CoverOperation { } message CoverStateResponse { option (id) = 22; + option (base_class) = "StateResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_COVER"; option (no_delay) = true; @@ -335,6 +359,7 @@ message CoverStateResponse { float position = 3; float tilt = 4; CoverOperation current_operation = 5; + uint32 device_id = 6; } enum LegacyCoverCommand { @@ -365,6 +390,7 @@ message CoverCommandRequest { // ==================== FAN ==================== message ListEntitiesFanResponse { option (id) = 14; + option (base_class) = "InfoResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_FAN"; @@ -380,6 +406,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; @@ -392,6 +419,7 @@ enum FanDirection { } message FanStateResponse { option (id) = 23; + option (base_class) = "StateResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_FAN"; option (no_delay) = true; @@ -403,6 +431,7 @@ message FanStateResponse { FanDirection direction = 5; int32 speed_level = 6; string preset_mode = 7; + uint32 device_id = 8; } message FanCommandRequest { option (id) = 31; @@ -441,6 +470,7 @@ enum ColorMode { } message ListEntitiesLightResponse { option (id) = 15; + option (base_class) = "InfoResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_LIGHT"; @@ -460,9 +490,11 @@ message ListEntitiesLightResponse { bool disabled_by_default = 13; string icon = 14; EntityCategory entity_category = 15; + uint32 device_id = 16; } message LightStateResponse { option (id) = 24; + option (base_class) = "StateResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_LIGHT"; option (no_delay) = true; @@ -480,6 +512,7 @@ message LightStateResponse { float cold_white = 12; float warm_white = 13; string effect = 9; + uint32 device_id = 14; } message LightCommandRequest { option (id) = 32; @@ -532,6 +565,7 @@ enum SensorLastResetType { message ListEntitiesSensorResponse { option (id) = 16; + option (base_class) = "InfoResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_SENSOR"; @@ -549,9 +583,11 @@ 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; + option (base_class) = "StateResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_SENSOR"; option (no_delay) = true; @@ -561,11 +597,13 @@ 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 ==================== message ListEntitiesSwitchResponse { option (id) = 17; + option (base_class) = "InfoResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_SWITCH"; @@ -578,15 +616,18 @@ message ListEntitiesSwitchResponse { bool disabled_by_default = 7; EntityCategory entity_category = 8; string device_class = 9; + uint32 device_id = 10; } message SwitchStateResponse { option (id) = 26; + option (base_class) = "StateResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_SWITCH"; option (no_delay) = true; fixed32 key = 1; bool state = 2; + uint32 device_id = 3; } message SwitchCommandRequest { option (id) = 33; @@ -601,6 +642,7 @@ message SwitchCommandRequest { // ==================== TEXT SENSOR ==================== message ListEntitiesTextSensorResponse { option (id) = 18; + option (base_class) = "InfoResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_TEXT_SENSOR"; @@ -612,9 +654,11 @@ message ListEntitiesTextSensorResponse { bool disabled_by_default = 6; EntityCategory entity_category = 7; string device_class = 8; + uint32 device_id = 9; } message TextSensorStateResponse { option (id) = 27; + option (base_class) = "StateResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_TEXT_SENSOR"; option (no_delay) = true; @@ -624,6 +668,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 ==================== @@ -782,8 +827,9 @@ message ExecuteServiceRequest { // ==================== CAMERA ==================== 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; @@ -791,12 +837,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; @@ -805,7 +852,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; @@ -861,6 +908,7 @@ enum ClimatePreset { } message ListEntitiesClimateResponse { option (id) = 46; + option (base_class) = "InfoResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_CLIMATE"; @@ -891,9 +939,11 @@ 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; + option (base_class) = "StateResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_CLIMATE"; option (no_delay) = true; @@ -914,6 +964,7 @@ message ClimateStateResponse { string custom_preset = 13; float current_humidity = 14; float target_humidity = 15; + uint32 device_id = 16; } message ClimateCommandRequest { option (id) = 48; @@ -955,6 +1006,7 @@ enum NumberMode { } message ListEntitiesNumberResponse { option (id) = 49; + option (base_class) = "InfoResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_NUMBER"; @@ -971,9 +1023,11 @@ message ListEntitiesNumberResponse { string unit_of_measurement = 11; NumberMode mode = 12; string device_class = 13; + uint32 device_id = 14; } message NumberStateResponse { option (id) = 50; + option (base_class) = "StateResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_NUMBER"; option (no_delay) = true; @@ -983,6 +1037,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; @@ -997,6 +1052,7 @@ message NumberCommandRequest { // ==================== SELECT ==================== message ListEntitiesSelectResponse { option (id) = 52; + option (base_class) = "InfoResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_SELECT"; @@ -1008,9 +1064,11 @@ message ListEntitiesSelectResponse { repeated string options = 6; bool disabled_by_default = 7; EntityCategory entity_category = 8; + uint32 device_id = 9; } message SelectStateResponse { option (id) = 53; + option (base_class) = "StateResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_SELECT"; option (no_delay) = true; @@ -1020,6 +1078,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; @@ -1034,6 +1093,7 @@ message SelectCommandRequest { // ==================== SIREN ==================== message ListEntitiesSirenResponse { option (id) = 55; + option (base_class) = "InfoResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_SIREN"; @@ -1047,15 +1107,18 @@ message ListEntitiesSirenResponse { bool supports_duration = 8; bool supports_volume = 9; EntityCategory entity_category = 10; + uint32 device_id = 11; } message SirenStateResponse { option (id) = 56; + option (base_class) = "StateResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_SIREN"; option (no_delay) = true; fixed32 key = 1; bool state = 2; + uint32 device_id = 3; } message SirenCommandRequest { option (id) = 57; @@ -1090,6 +1153,7 @@ enum LockCommand { } message ListEntitiesLockResponse { option (id) = 58; + option (base_class) = "InfoResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_LOCK"; @@ -1107,14 +1171,17 @@ message ListEntitiesLockResponse { // Not yet implemented: string code_format = 11; + uint32 device_id = 12; } message LockStateResponse { option (id) = 59; + option (base_class) = "StateResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_LOCK"; option (no_delay) = true; fixed32 key = 1; LockState state = 2; + uint32 device_id = 3; } message LockCommandRequest { option (id) = 60; @@ -1132,6 +1199,7 @@ message LockCommandRequest { // ==================== BUTTON ==================== message ListEntitiesButtonResponse { option (id) = 61; + option (base_class) = "InfoResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_BUTTON"; @@ -1143,6 +1211,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; @@ -1182,6 +1251,7 @@ message MediaPlayerSupportedFormat { } message ListEntitiesMediaPlayerResponse { option (id) = 63; + option (base_class) = "InfoResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_MEDIA_PLAYER"; @@ -1196,9 +1266,12 @@ message ListEntitiesMediaPlayerResponse { bool supports_pause = 8; repeated MediaPlayerSupportedFormat supported_formats = 9; + + uint32 device_id = 10; } message MediaPlayerStateResponse { option (id) = 64; + option (base_class) = "StateResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_MEDIA_PLAYER"; option (no_delay) = true; @@ -1206,6 +1279,7 @@ message MediaPlayerStateResponse { MediaPlayerState state = 2; float volume = 3; bool muted = 4; + uint32 device_id = 5; } message MediaPlayerCommandRequest { option (id) = 65; @@ -1600,6 +1674,7 @@ enum VoiceAssistantEvent { VOICE_ASSISTANT_STT_VAD_END = 12; VOICE_ASSISTANT_TTS_STREAM_START = 98; VOICE_ASSISTANT_TTS_STREAM_END = 99; + VOICE_ASSISTANT_INTENT_PROGRESS = 100; } message VoiceAssistantEventData { @@ -1720,6 +1795,7 @@ enum AlarmControlPanelStateCommand { message ListEntitiesAlarmControlPanelResponse { option (id) = 94; + option (base_class) = "InfoResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_ALARM_CONTROL_PANEL"; @@ -1732,15 +1808,18 @@ message ListEntitiesAlarmControlPanelResponse { uint32 supported_features = 8; bool requires_code = 9; bool requires_code_to_arm = 10; + uint32 device_id = 11; } message AlarmControlPanelStateResponse { option (id) = 95; + option (base_class) = "StateResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_ALARM_CONTROL_PANEL"; option (no_delay) = true; fixed32 key = 1; AlarmControlPanelState state = 2; + uint32 device_id = 3; } message AlarmControlPanelCommandRequest { @@ -1760,6 +1839,7 @@ enum TextMode { } message ListEntitiesTextResponse { option (id) = 97; + option (base_class) = "InfoResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_TEXT"; @@ -1774,9 +1854,11 @@ message ListEntitiesTextResponse { uint32 max_length = 9; string pattern = 10; TextMode mode = 11; + uint32 device_id = 12; } message TextStateResponse { option (id) = 98; + option (base_class) = "StateResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_TEXT"; option (no_delay) = true; @@ -1786,6 +1868,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; @@ -1801,6 +1884,7 @@ message TextCommandRequest { // ==================== DATETIME DATE ==================== message ListEntitiesDateResponse { option (id) = 100; + option (base_class) = "InfoResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_DATETIME_DATE"; @@ -1811,9 +1895,11 @@ message ListEntitiesDateResponse { string icon = 5; bool disabled_by_default = 6; EntityCategory entity_category = 7; + uint32 device_id = 8; } message DateStateResponse { option (id) = 101; + option (base_class) = "StateResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_DATETIME_DATE"; option (no_delay) = true; @@ -1825,6 +1911,7 @@ message DateStateResponse { uint32 year = 3; uint32 month = 4; uint32 day = 5; + uint32 device_id = 6; } message DateCommandRequest { option (id) = 102; @@ -1841,6 +1928,7 @@ message DateCommandRequest { // ==================== DATETIME TIME ==================== message ListEntitiesTimeResponse { option (id) = 103; + option (base_class) = "InfoResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_DATETIME_TIME"; @@ -1851,9 +1939,11 @@ message ListEntitiesTimeResponse { string icon = 5; bool disabled_by_default = 6; EntityCategory entity_category = 7; + uint32 device_id = 8; } message TimeStateResponse { option (id) = 104; + option (base_class) = "StateResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_DATETIME_TIME"; option (no_delay) = true; @@ -1865,6 +1955,7 @@ message TimeStateResponse { uint32 hour = 3; uint32 minute = 4; uint32 second = 5; + uint32 device_id = 6; } message TimeCommandRequest { option (id) = 105; @@ -1881,6 +1972,7 @@ message TimeCommandRequest { // ==================== EVENT ==================== message ListEntitiesEventResponse { option (id) = 107; + option (base_class) = "InfoResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_EVENT"; @@ -1894,19 +1986,23 @@ message ListEntitiesEventResponse { string device_class = 8; repeated string event_types = 9; + uint32 device_id = 10; } message EventResponse { option (id) = 108; + option (base_class) = "StateResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_EVENT"; fixed32 key = 1; string event_type = 2; + uint32 device_id = 3; } // ==================== VALVE ==================== message ListEntitiesValveResponse { option (id) = 109; + option (base_class) = "InfoResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_VALVE"; @@ -1922,6 +2018,7 @@ message ListEntitiesValveResponse { bool assumed_state = 9; bool supports_position = 10; bool supports_stop = 11; + uint32 device_id = 12; } enum ValveOperation { @@ -1931,6 +2028,7 @@ enum ValveOperation { } message ValveStateResponse { option (id) = 110; + option (base_class) = "StateResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_VALVE"; option (no_delay) = true; @@ -1938,6 +2036,7 @@ message ValveStateResponse { fixed32 key = 1; float position = 2; ValveOperation current_operation = 3; + uint32 device_id = 4; } message ValveCommandRequest { @@ -1955,6 +2054,7 @@ message ValveCommandRequest { // ==================== DATETIME DATETIME ==================== message ListEntitiesDateTimeResponse { option (id) = 112; + option (base_class) = "InfoResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_DATETIME_DATETIME"; @@ -1965,9 +2065,11 @@ message ListEntitiesDateTimeResponse { string icon = 5; bool disabled_by_default = 6; EntityCategory entity_category = 7; + uint32 device_id = 8; } message DateTimeStateResponse { option (id) = 113; + option (base_class) = "StateResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_DATETIME_DATETIME"; option (no_delay) = true; @@ -1977,6 +2079,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; @@ -1991,6 +2094,7 @@ message DateTimeCommandRequest { // ==================== UPDATE ==================== message ListEntitiesUpdateResponse { option (id) = 116; + option (base_class) = "InfoResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_UPDATE"; @@ -2002,9 +2106,11 @@ message ListEntitiesUpdateResponse { bool disabled_by_default = 6; EntityCategory entity_category = 7; string device_class = 8; + uint32 device_id = 9; } message UpdateStateResponse { option (id) = 117; + option (base_class) = "StateResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_UPDATE"; option (no_delay) = true; @@ -2019,6 +2125,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 e70fc5ae74..9b4ec2ed50 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -28,8 +28,19 @@ 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 APIConnection::APIConnection(std::unique_ptr sock, APIServer *parent) : parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) { @@ -47,6 +58,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 +70,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 +96,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 +198,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 +226,8 @@ void APIConnection::loop() { if (this->send_message(resp)) { state_subs_at_++; } + } else { + state_subs_at_ = -1; } } } @@ -229,50 +236,70 @@ 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 size = 0; - msg.calculate_size(size); + uint32_t calculated_size = 0; + msg.calculate_size(calculated_size); + + // Cache frame sizes to avoid repeated virtual calls + const uint8_t header_padding = conn->helper_->frame_header_padding(); + const uint8_t footer_size = conn->helper_->frame_footer_size(); // Calculate total size with padding for buffer allocation - uint16_t total_size = - static_cast(size) + conn->helper_->frame_header_padding() + conn->helper_->frame_footer_size(); + size_t total_calculated_size = calculated_size + header_padding + footer_size; // Check if it fits - if (total_size > remaining_size) { + if (total_calculated_size > remaining_size) { return 0; // Doesn't fit } - // Allocate exact buffer space needed (just the payload, not the overhead) - ProtoWriteBuffer buffer = - is_single ? conn->allocate_single_message_buffer(size) : conn->allocate_batch_message_buffer(size); + // Allocate buffer space - pass payload size, allocation functions add header/footer space + ProtoWriteBuffer buffer = is_single ? conn->allocate_single_message_buffer(calculated_size) + : conn->allocate_batch_message_buffer(calculated_size); + + // Get buffer size after allocation (which includes header padding) + std::vector &shared_buf = conn->parent_->get_shared_buffer_ref(); + size_t size_before_encode = shared_buf.size(); // Encode directly into buffer msg.encode(buffer); - return total_size; + + // Calculate actual encoded size (not including header that was already added) + size_t actual_payload_size = shared_buf.size() - size_before_encode; + + // Return actual total size (header + actual payload + footer) + size_t actual_total_size = header_padding + actual_payload_size + footer_size; + + // Verify that calculate_size() returned the correct value + assert(calculated_size == actual_payload_size); + return static_cast(actual_total_size); } #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, @@ -281,7 +308,7 @@ uint16_t APIConnection::try_send_binary_sensor_state(EntityBase *entity, APIConn BinarySensorStateResponse resp; resp.state = binary_sensor->state; resp.missing_state = !binary_sensor->has_state(); - resp.key = binary_sensor->get_object_id_hash(); + fill_entity_state_base(binary_sensor, resp); return encode_message_to_buffer(resp, BinarySensorStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } @@ -298,10 +325,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) { @@ -314,7 +338,7 @@ uint16_t APIConnection::try_send_cover_state(EntityBase *entity, APIConnection * if (traits.get_supports_tilt()) msg.tilt = cover->tilt; msg.current_operation = static_cast(cover->current_operation); - msg.key = cover->get_object_id_hash(); + fill_entity_state_base(cover, msg); return encode_message_to_buffer(msg, CoverStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, @@ -361,10 +385,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) { @@ -381,7 +402,7 @@ uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *co msg.direction = static_cast(fan->direction); if (traits.supports_preset_modes()) msg.preset_mode = fan->preset_mode; - msg.key = fan->get_object_id_hash(); + fill_entity_state_base(fan, msg); return encode_message_to_buffer(msg, FanStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, @@ -422,10 +443,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) { @@ -447,7 +465,7 @@ uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection * resp.warm_white = values.get_warm_white(); if (light->supports_effects()) resp.effect = light->get_effect_name(); - resp.key = light->get_object_id_hash(); + fill_entity_state_base(light, resp); return encode_message_to_buffer(resp, LightStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, @@ -516,10 +534,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, @@ -528,7 +543,7 @@ uint16_t APIConnection::try_send_sensor_state(EntityBase *entity, APIConnection SensorStateResponse resp; resp.state = sensor->state; resp.missing_state = !sensor->has_state(); - resp.key = sensor->get_object_id_hash(); + fill_entity_state_base(sensor, resp); return encode_message_to_buffer(resp, SensorStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } @@ -548,10 +563,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, @@ -559,7 +571,7 @@ uint16_t APIConnection::try_send_switch_state(EntityBase *entity, APIConnection auto *a_switch = static_cast(entity); SwitchStateResponse resp; resp.state = a_switch->state; - resp.key = a_switch->get_object_id_hash(); + fill_entity_state_base(a_switch, resp); return encode_message_to_buffer(resp, SwitchStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } @@ -587,12 +599,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, @@ -601,7 +609,7 @@ uint16_t APIConnection::try_send_text_sensor_state(EntityBase *entity, APIConnec TextSensorStateResponse resp; resp.state = text_sensor->state; resp.missing_state = !text_sensor->has_state(); - resp.key = text_sensor->get_object_id_hash(); + fill_entity_state_base(text_sensor, resp); return encode_message_to_buffer(resp, TextSensorStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } uint16_t APIConnection::try_send_text_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, @@ -616,13 +624,13 @@ 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) { auto *climate = static_cast(entity); ClimateStateResponse resp; - resp.key = climate->get_object_id_hash(); + fill_entity_state_base(climate, resp); auto traits = climate->get_traits(); resp.mode = static_cast(climate->mode); resp.action = static_cast(climate->action); @@ -651,9 +659,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); @@ -718,10 +723,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, @@ -730,7 +732,7 @@ uint16_t APIConnection::try_send_number_state(EntityBase *entity, APIConnection NumberStateResponse resp; resp.state = number->state; resp.missing_state = !number->has_state(); - resp.key = number->get_object_id_hash(); + fill_entity_state_base(number, resp); return encode_message_to_buffer(resp, NumberStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } @@ -760,7 +762,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) { @@ -770,12 +772,9 @@ uint16_t APIConnection::try_send_date_state(EntityBase *entity, APIConnection *c resp.year = date->year; resp.month = date->month; resp.day = date->day; - resp.key = date->get_object_id_hash(); + 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); @@ -796,7 +795,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) { @@ -806,12 +805,9 @@ uint16_t APIConnection::try_send_time_state(EntityBase *entity, APIConnection *c resp.hour = time->hour; resp.minute = time->minute; resp.second = time->second; - resp.key = time->get_object_id_hash(); + 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); @@ -832,8 +828,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) { @@ -844,12 +840,9 @@ uint16_t APIConnection::try_send_datetime_state(EntityBase *entity, APIConnectio ESPTime state = datetime->state_as_esptime(); resp.epoch_seconds = state.timestamp; } - resp.key = datetime->get_object_id_hash(); + 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); @@ -870,10 +863,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, @@ -882,7 +872,7 @@ uint16_t APIConnection::try_send_text_state(EntityBase *entity, APIConnection *c TextStateResponse resp; resp.state = text->state; resp.missing_state = !text->has_state(); - resp.key = text->get_object_id_hash(); + fill_entity_state_base(text, resp); return encode_message_to_buffer(resp, TextStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } @@ -910,10 +900,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, @@ -922,7 +909,7 @@ uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection SelectStateResponse resp; resp.state = select->state; resp.missing_state = !select->has_state(); - resp.key = select->get_object_id_hash(); + fill_entity_state_base(select, resp); return encode_message_to_buffer(resp, SelectStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } @@ -947,9 +934,6 @@ void APIConnection::select_command(const SelectCommandRequest &msg) { #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); @@ -969,10 +953,7 @@ void esphome::api::APIConnection::button_command(const ButtonCommandRequest &msg #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, @@ -980,7 +961,7 @@ uint16_t APIConnection::try_send_lock_state(EntityBase *entity, APIConnection *c auto *a_lock = static_cast(entity); LockStateResponse resp; resp.state = static_cast(a_lock->state); - resp.key = a_lock->get_object_id_hash(); + fill_entity_state_base(a_lock, resp); return encode_message_to_buffer(resp, LockStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } @@ -1015,7 +996,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) { @@ -1023,12 +1004,9 @@ uint16_t APIConnection::try_send_valve_state(EntityBase *entity, APIConnection * ValveStateResponse resp; resp.position = valve->position; resp.current_operation = static_cast(valve->current_operation); - resp.key = valve->get_object_id_hash(); + 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); @@ -1057,8 +1035,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) { @@ -1070,13 +1048,9 @@ uint16_t APIConnection::try_send_media_player_state(EntityBase *entity, APIConne resp.state = static_cast(report_state); resp.volume = media_player->volume; resp.muted = media_player->is_muted(); - resp.key = media_player->get_object_id_hash(); + 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); @@ -1117,38 +1091,35 @@ 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; 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 @@ -1324,21 +1295,17 @@ 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) { auto *a_alarm_control_panel = static_cast(entity); AlarmControlPanelStateResponse resp; resp.state = static_cast(a_alarm_control_panel->get_state()); - resp.key = a_alarm_control_panel->get_object_id_hash(); + 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); @@ -1386,16 +1353,13 @@ 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) { EventResponse resp; resp.event_type = event_type; - resp.key = event->get_object_id_hash(); + fill_entity_state_base(event, resp); return encode_message_to_buffer(resp, EventResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } @@ -1413,7 +1377,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) { @@ -1432,12 +1396,9 @@ uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection resp.release_summary = update->update_info.summary; resp.release_url = update->update_info.release_url; } - resp.key = update->get_object_id_hash(); + 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); @@ -1469,7 +1430,7 @@ 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) + if (this->flags_.log_subscription < level) return false; // Pre-calculate message size to avoid reallocations @@ -1492,14 +1453,13 @@ bool APIConnection::try_send_log_message(int level, const char *tag, const char buffer.encode_string(3, line, line_length); // string message = 3 // SubscribeLogsResponse - 29 - return this->send_buffer(buffer, 29); + return this->send_buffer(buffer, SubscribeLogsResponse::MESSAGE_TYPE); } 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(), @@ -1511,19 +1471,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(); @@ -1534,7 +1499,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(); @@ -1547,6 +1516,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) @@ -1574,6 +1545,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; } @@ -1619,7 +1607,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; @@ -1627,7 +1615,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; } @@ -1639,7 +1627,7 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) { return false; } bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint16_t message_type) { - if (!this->try_to_clear_buffer(message_type != 29)) { // SubscribeLogsResponse + if (!this->try_to_clear_buffer(message_type != SubscribeLogsResponse::MESSAGE_TYPE)) { // SubscribeLogsResponse return false; } @@ -1649,10 +1637,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; } @@ -1661,15 +1649,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) { @@ -1678,7 +1666,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; } @@ -1688,9 +1678,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; @@ -1699,14 +1694,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; } @@ -1716,22 +1711,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; } @@ -1749,7 +1750,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); } @@ -1758,10 +1760,10 @@ 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; - uint32_t remaining_size = MAX_PACKET_SIZE; + uint16_t remaining_size = std::numeric_limits::max(); // Track where each message's header padding begins in the buffer // For plaintext: this is where the 6-byte header padding starts @@ -1770,10 +1772,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 @@ -1786,11 +1789,15 @@ void APIConnection::process_batch_() { packet_info.emplace_back(item.message_type, current_offset, proto_payload_size); // Update tracking variables + items_processed++; + // After first message, set remaining size to MAX_PACKET_SIZE to avoid fragmentation + if (items_processed == 1) { + remaining_size = MAX_PACKET_SIZE; + } remaining_size -= payload_size; // Calculate where the next message's header padding will start // Current buffer size + footer space (that prepare_message_buffer will add for this message) current_offset = this->parent_->get_shared_buffer_ref().size() + footer_size; - items_processed++; } if (items_processed == 0) { @@ -1810,44 +1817,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, @@ -1862,6 +1871,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 34c7dcd880..166dbc3656 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); 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; @@ -240,8 +223,8 @@ class APIConnection : public APIServerConnection { // - Header padding: space for protocol headers (7 bytes for Noise, 6 for Plaintext) // - Footer: space for MAC (16 bytes for Noise, 0 for Plaintext) shared_buf.reserve(reserve_size + header_padding + this->helper_->frame_footer_size()); - // Insert header padding bytes so message encoding starts at the correct position - shared_buf.insert(shared_buf.begin(), header_padding, 0); + // Resize to add header padding so message encoding starts at the correct position + shared_buf.resize(header_padding); return {&shared_buf}; } @@ -249,47 +232,47 @@ class APIConnection : public APIServerConnection { ProtoWriteBuffer prepare_message_buffer(uint16_t message_size, bool is_first_message) { // Get reference to shared buffer (it maintains state between batch messages) std::vector &shared_buf = this->parent_->get_shared_buffer_ref(); - size_t current_size = shared_buf.size(); if (is_first_message) { - // For first message, initialize buffer with header padding - uint8_t header_padding = this->helper_->frame_header_padding(); shared_buf.clear(); - shared_buf.reserve(message_size + header_padding); - shared_buf.resize(header_padding); - // Fill header padding with zeros - std::fill(shared_buf.begin(), shared_buf.end(), 0); - } else { - // For subsequent messages, add footer space for previous message and header for this message - uint8_t footer_size = this->helper_->frame_footer_size(); - uint8_t header_padding = this->helper_->frame_header_padding(); - - // Reserve additional space for everything - shared_buf.reserve(current_size + footer_size + header_padding + message_size); - - // Single resize to add both footer and header padding - size_t new_size = current_size + footer_size + header_padding; - shared_buf.resize(new_size); - - // Fill the newly added bytes with zeros (footer + header padding) - std::fill(shared_buf.begin() + current_size, shared_buf.end(), 0); } + size_t current_size = shared_buf.size(); + + // Calculate padding to add: + // - First message: just header padding + // - Subsequent messages: footer for previous message + header padding for this message + size_t padding_to_add = is_first_message + ? this->helper_->frame_header_padding() + : this->helper_->frame_header_padding() + this->helper_->frame_footer_size(); + + // Reserve space for padding + message + shared_buf.reserve(current_size + padding_to_add + message_size); + + // Resize to add the padding bytes + shared_buf.resize(current_size + padding_to_add); + return {&shared_buf}; } 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); ProtoWriteBuffer allocate_batch_message_buffer(uint16_t size); protected: - // Helper function to fill common entity fields - template static void fill_entity_info_base(esphome::EntityBase *entity, ResponseT &response) { + // Helper function to fill common entity info fields + static void fill_entity_info_base(esphome::EntityBase *entity, InfoResponseProtoMessage &response) { // Set common fields that are shared by all entity types response.key = entity->get_object_id_hash(); response.object_id = entity->get_object_id(); @@ -301,12 +284,37 @@ 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); + // 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); @@ -417,7 +425,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 @@ -433,124 +441,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 @@ -567,24 +533,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. @@ -601,9 +629,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) { @@ -615,6 +683,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..6ed9c95354 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; @@ -274,17 +285,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 @@ -330,17 +345,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) { @@ -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 @@ -1026,18 +1019,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 +1033,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 +1069,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..1bb6bc7ed3 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,15 +138,41 @@ 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_(); @@ -200,7 +195,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 +208,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 +249,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 +273,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_options.proto b/esphome/components/api/api_options.proto index feaf39ba15..3a547b8688 100644 --- a/esphome/components/api/api_options.proto +++ b/esphome/components/api/api_options.proto @@ -21,4 +21,5 @@ extend google.protobuf.MessageOptions { optional string ifdef = 1038; optional bool log = 1039 [default=true]; optional bool no_delay = 1040 [default=false]; + optional string base_class = 1041; } diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 55677f678b..3505ec758d 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -3,631 +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"; - 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: { @@ -662,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: { @@ -722,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: { @@ -760,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: { @@ -784,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: { @@ -903,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; } @@ -927,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); @@ -948,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: { @@ -1065,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; } @@ -1079,6 +357,10 @@ bool ListEntitiesBinarySensorResponse::decode_length(uint32_t field_id, ProtoLen this->name = value.as_string(); return true; } + case 4: { + this->unique_id = value.as_string(); + return true; + } case 5: { this->device_class = value.as_string(); return true; @@ -1105,61 +387,26 @@ void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); buffer.encode_string(5, this->device_class); buffer.encode_bool(6, this->is_status_binary_sensor); 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); ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_string_field(total_size, 1, this->unique_id, false); ProtoSize::add_string_field(total_size, 1, this->device_class, false); ProtoSize::add_bool_field(total_size, 1, this->is_status_binary_sensor, false); 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(" 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: { @@ -1170,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; } @@ -1188,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: { @@ -1239,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; } @@ -1253,6 +493,10 @@ bool ListEntitiesCoverResponse::decode_length(uint32_t field_id, ProtoLengthDeli this->name = value.as_string(); return true; } + case 4: { + this->unique_id = value.as_string(); + return true; + } case 8: { this->device_class = value.as_string(); return true; @@ -1279,6 +523,7 @@ void ListEntitiesCoverResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); buffer.encode_bool(5, this->assumed_state); buffer.encode_bool(6, this->supports_position); buffer.encode_bool(7, this->supports_tilt); @@ -1287,11 +532,13 @@ 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); ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_string_field(total_size, 1, this->unique_id, false); 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_tilt, false); @@ -1300,58 +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(" 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: { @@ -1362,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; } @@ -1390,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); @@ -1397,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: { @@ -1491,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: { @@ -1558,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; } @@ -1572,6 +711,10 @@ bool ListEntitiesFanResponse::decode_length(uint32_t field_id, ProtoLengthDelimi this->name = value.as_string(); return true; } + case 4: { + this->unique_id = value.as_string(); + return true; + } case 10: { this->icon = value.as_string(); return true; @@ -1598,6 +741,7 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); buffer.encode_bool(5, this->supports_oscillation); buffer.encode_bool(6, this->supports_speed); buffer.encode_bool(7, this->supports_direction); @@ -1608,11 +752,13 @@ 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); ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_string_field(total_size, 1, this->unique_id, false); ProtoSize::add_bool_field(total_size, 1, this->supports_oscillation, false); ProtoSize::add_bool_field(total_size, 1, this->supports_speed, false); ProtoSize::add_bool_field(total_size, 1, this->supports_direction, false); @@ -1625,61 +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(" 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: { @@ -1702,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; } @@ -1734,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); @@ -1743,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: { @@ -1880,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: { @@ -1970,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; } @@ -1984,6 +993,10 @@ bool ListEntitiesLightResponse::decode_length(uint32_t field_id, ProtoLengthDeli this->name = value.as_string(); return true; } + case 4: { + this->unique_id = value.as_string(); + return true; + } case 11: { this->effects.push_back(value.as_string()); return true; @@ -2018,6 +1031,7 @@ void ListEntitiesLightResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); for (auto &it : this->supported_color_modes) { buffer.encode_enum(12, it, true); } @@ -2033,11 +1047,13 @@ 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); ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_string_field(total_size, 1, this->unique_id, false); if (!this->supported_color_modes.empty()) { for (const auto &it : this->supported_color_modes) { ProtoSize::add_enum_field(total_size, 1, static_cast(it), true); @@ -2057,76 +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"); - - 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: { @@ -2137,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; } @@ -2211,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); @@ -2226,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: { @@ -2479,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: { @@ -2631,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; } @@ -2645,6 +1411,10 @@ bool ListEntitiesSensorResponse::decode_length(uint32_t field_id, ProtoLengthDel this->name = value.as_string(); return true; } + case 4: { + this->unique_id = value.as_string(); + return true; + } case 5: { this->icon = value.as_string(); return true; @@ -2675,6 +1445,7 @@ void ListEntitiesSensorResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); buffer.encode_string(5, this->icon); buffer.encode_string(6, this->unit_of_measurement); buffer.encode_int32(7, this->accuracy_decimals); @@ -2684,11 +1455,13 @@ 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); ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_string_field(total_size, 1, this->unique_id, false); ProtoSize::add_string_field(total_size, 1, this->icon, false); ProtoSize::add_string_field(total_size, 1, this->unit_of_measurement, false); ProtoSize::add_int32_field(total_size, 1, this->accuracy_decimals, false); @@ -2698,69 +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(" 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; } @@ -2783,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: { @@ -2823,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; } @@ -2837,6 +1547,10 @@ bool ListEntitiesSwitchResponse::decode_length(uint32_t field_id, ProtoLengthDel this->name = value.as_string(); return true; } + case 4: { + this->unique_id = value.as_string(); + return true; + } case 5: { this->icon = value.as_string(); return true; @@ -2863,67 +1577,36 @@ void ListEntitiesSwitchResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); buffer.encode_string(5, this->icon); buffer.encode_bool(6, this->assumed_state); 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); ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_string_field(total_size, 1, this->unique_id, false); ProtoSize::add_string_field(total_size, 1, this->icon, false); ProtoSize::add_bool_field(total_size, 1, this->assumed_state, 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_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(" 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; } @@ -2941,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: { @@ -2989,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: { @@ -3014,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; } @@ -3028,6 +1689,10 @@ bool ListEntitiesTextSensorResponse::decode_length(uint32_t field_id, ProtoLengt this->name = value.as_string(); return true; } + case 4: { + this->unique_id = value.as_string(); + return true; + } case 5: { this->icon = value.as_string(); return true; @@ -3054,61 +1719,34 @@ void ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); buffer.encode_string(5, this->icon); 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); ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_string_field(total_size, 1, this->unique_id, false); 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_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(" 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; } @@ -3137,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) { @@ -3184,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: { @@ -3224,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 { @@ -3232,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: { @@ -3260,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: { @@ -3288,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) { @@ -3327,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: { @@ -3393,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: { @@ -3466,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: { @@ -3512,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: { @@ -3549,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: { @@ -3588,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: { @@ -3638,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: { @@ -3759,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: { @@ -3844,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: { @@ -3871,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; } @@ -3885,6 +2245,10 @@ bool ListEntitiesCameraResponse::decode_length(uint32_t field_id, ProtoLengthDel this->name = value.as_string(); return true; } + case 4: { + this->unique_id = value.as_string(); + return true; + } case 6: { this->icon = value.as_string(); return true; @@ -3907,49 +2271,22 @@ void ListEntitiesCameraResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); 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); ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_string_field(total_size, 1, this->unique_id, false); 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(" 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: { @@ -3982,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 { @@ -3990,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: { @@ -4031,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: { @@ -4095,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; } @@ -4109,6 +2419,10 @@ bool ListEntitiesClimateResponse::decode_length(uint32_t field_id, ProtoLengthDe this->name = value.as_string(); return true; } + case 4: { + this->unique_id = value.as_string(); + return true; + } case 15: { this->supported_custom_fan_modes.push_back(value.as_string()); return true; @@ -4163,6 +2477,7 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); buffer.encode_bool(5, this->supports_current_temperature); buffer.encode_bool(6, this->supports_two_point_target_temperature); for (auto &it : this->supported_modes) { @@ -4196,11 +2511,13 @@ 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); ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_string_field(total_size, 1, this->unique_id, false); ProtoSize::add_bool_field(total_size, 1, this->supports_current_temperature, false); ProtoSize::add_bool_field(total_size, 1, this->supports_two_point_target_temperature, false); if (!this->supported_modes.empty()) { @@ -4246,128 +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(" 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: { @@ -4394,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; } @@ -4462,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); @@ -4479,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: { @@ -4713,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: { @@ -4830,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; } @@ -4844,6 +2877,10 @@ bool ListEntitiesNumberResponse::decode_length(uint32_t field_id, ProtoLengthDel this->name = value.as_string(); return true; } + case 4: { + this->unique_id = value.as_string(); + return true; + } case 5: { this->icon = value.as_string(); return true; @@ -4886,6 +2923,7 @@ void ListEntitiesNumberResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); buffer.encode_string(5, this->icon); buffer.encode_float(6, this->min_value); buffer.encode_float(7, this->max_value); @@ -4895,11 +2933,13 @@ 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); ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_string_field(total_size, 1, this->unique_id, false); ProtoSize::add_string_field(total_size, 1, this->icon, false); ProtoSize::add_fixed_field<4>(total_size, 1, this->min_value != 0.0f, false); ProtoSize::add_fixed_field<4>(total_size, 1, this->max_value != 0.0f, false); @@ -4909,71 +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(" 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; } @@ -4996,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: { @@ -5044,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: { @@ -5070,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; } @@ -5084,6 +3043,10 @@ bool ListEntitiesSelectResponse::decode_length(uint32_t field_id, ProtoLengthDel this->name = value.as_string(); return true; } + case 4: { + this->unique_id = value.as_string(); + return true; + } case 5: { this->icon = value.as_string(); return true; @@ -5110,17 +3073,20 @@ void ListEntitiesSelectResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); buffer.encode_string(5, this->icon); for (auto &it : this->options) { buffer.encode_string(6, it, true); } 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); ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_string_field(total_size, 1, this->unique_id, false); ProtoSize::add_string_field(total_size, 1, this->icon, false); if (!this->options.empty()) { for (const auto &it : this->options) { @@ -5129,50 +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(" 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; } @@ -5201,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: { @@ -5254,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: { @@ -5287,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; } @@ -5301,6 +3209,10 @@ bool ListEntitiesSirenResponse::decode_length(uint32_t field_id, ProtoLengthDeli this->name = value.as_string(); return true; } + case 4: { + this->unique_id = value.as_string(); + return true; + } case 5: { this->icon = value.as_string(); return true; @@ -5327,6 +3239,7 @@ void ListEntitiesSirenResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); buffer.encode_string(5, this->icon); buffer.encode_bool(6, this->disabled_by_default); for (auto &it : this->tones) { @@ -5335,11 +3248,13 @@ 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); ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_string_field(total_size, 1, this->unique_id, false); ProtoSize::add_string_field(total_size, 1, this->icon, false); ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); if (!this->tones.empty()) { @@ -5350,58 +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(" 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; } @@ -5419,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: { @@ -5515,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: { @@ -5582,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; } @@ -5596,6 +3419,10 @@ bool ListEntitiesLockResponse::decode_length(uint32_t field_id, ProtoLengthDelim this->name = value.as_string(); return true; } + case 4: { + this->unique_id = value.as_string(); + return true; + } case 5: { this->icon = value.as_string(); return true; @@ -5622,6 +3449,7 @@ void ListEntitiesLockResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); buffer.encode_string(5, this->icon); buffer.encode_bool(6, this->disabled_by_default); buffer.encode_enum(7, this->entity_category); @@ -5629,11 +3457,13 @@ 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); ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_string_field(total_size, 1, this->unique_id, false); 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); @@ -5641,60 +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(" 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; } @@ -5712,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: { @@ -5778,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: { @@ -5811,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; } @@ -5825,6 +3583,10 @@ bool ListEntitiesButtonResponse::decode_length(uint32_t field_id, ProtoLengthDel this->name = value.as_string(); return true; } + case 4: { + this->unique_id = value.as_string(); + return true; + } case 5: { this->icon = value.as_string(); return true; @@ -5851,55 +3613,24 @@ void ListEntitiesButtonResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); buffer.encode_string(5, this->icon); 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); ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_string_field(total_size, 1, this->unique_id, false); 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_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(" 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: { @@ -5914,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: { @@ -5971,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: { @@ -6014,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; } @@ -6028,6 +3725,10 @@ bool ListEntitiesMediaPlayerResponse::decode_length(uint32_t field_id, ProtoLeng this->name = value.as_string(); return true; } + case 4: { + this->unique_id = value.as_string(); + return true; + } case 5: { this->icon = value.as_string(); return true; @@ -6054,6 +3755,7 @@ void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); buffer.encode_string(5, this->icon); buffer.encode_bool(6, this->disabled_by_default); buffer.encode_enum(7, this->entity_category); @@ -6061,58 +3763,20 @@ 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); ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_string_field(total_size, 1, this->unique_id, false); 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_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(" 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: { @@ -6123,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; } @@ -6146,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: { @@ -6253,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: { @@ -6313,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: { @@ -6353,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); @@ -6364,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: { @@ -6427,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); @@ -6453,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: { @@ -6528,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); @@ -6536,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: { @@ -6579,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: { @@ -6625,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: { @@ -6683,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: { @@ -6722,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: { @@ -6761,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: { @@ -6827,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: { @@ -6898,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: { @@ -6952,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: { @@ -6985,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: { @@ -7018,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: { @@ -7061,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: { @@ -7120,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); @@ -7128,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: { @@ -7174,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: { @@ -7217,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: { @@ -7272,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: { @@ -7319,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: { @@ -7387,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: { @@ -7438,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: { @@ -7481,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: { @@ -7519,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: { @@ -7563,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: { @@ -7611,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: { @@ -7666,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: { @@ -7708,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: { @@ -7738,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: { @@ -7770,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: { @@ -7819,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: { @@ -7886,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: { @@ -7935,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: { @@ -7972,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: { @@ -8016,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: { @@ -8053,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: { @@ -8126,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: { @@ -8198,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: { @@ -8234,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: { @@ -8278,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: { @@ -8347,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: { @@ -8392,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: { @@ -8426,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; } @@ -8440,6 +5223,10 @@ bool ListEntitiesAlarmControlPanelResponse::decode_length(uint32_t field_id, Pro this->name = value.as_string(); return true; } + case 4: { + this->unique_id = value.as_string(); + return true; + } case 5: { this->icon = value.as_string(); return true; @@ -8462,74 +5249,38 @@ void ListEntitiesAlarmControlPanelResponse::encode(ProtoWriteBuffer buffer) cons buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); 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->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); ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_string_field(total_size, 1, this->unique_id, false); 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->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(" 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; } @@ -8547,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: { @@ -8607,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: { @@ -8648,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; } @@ -8662,6 +5387,10 @@ bool ListEntitiesTextResponse::decode_length(uint32_t field_id, ProtoLengthDelim this->name = value.as_string(); return true; } + case 4: { + this->unique_id = value.as_string(); + return true; + } case 5: { this->icon = value.as_string(); return true; @@ -8688,6 +5417,7 @@ void ListEntitiesTextResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); buffer.encode_string(5, this->icon); buffer.encode_bool(6, this->disabled_by_default); buffer.encode_enum(7, this->entity_category); @@ -8695,11 +5425,13 @@ 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); ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_string_field(total_size, 1, this->unique_id, false); 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); @@ -8707,62 +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(" 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; } @@ -8791,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: { @@ -8844,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: { @@ -8869,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; } @@ -8883,6 +5545,10 @@ bool ListEntitiesDateResponse::decode_length(uint32_t field_id, ProtoLengthDelim this->name = value.as_string(); return true; } + case 4: { + this->unique_id = value.as_string(); + return true; + } case 5: { this->icon = value.as_string(); return true; @@ -8905,49 +5571,22 @@ void ListEntitiesDateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); 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); ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_string_field(total_size, 1, this->unique_id, false); 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(" 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: { @@ -8966,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; } @@ -8986,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); @@ -8993,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: { @@ -9064,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: { @@ -9100,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; } @@ -9114,6 +5709,10 @@ bool ListEntitiesTimeResponse::decode_length(uint32_t field_id, ProtoLengthDelim this->name = value.as_string(); return true; } + case 4: { + this->unique_id = value.as_string(); + return true; + } case 5: { this->icon = value.as_string(); return true; @@ -9136,49 +5735,22 @@ void ListEntitiesTimeResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); 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); ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_string_field(total_size, 1, this->unique_id, false); 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(" 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: { @@ -9197,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; } @@ -9217,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); @@ -9224,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: { @@ -9295,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: { @@ -9331,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; } @@ -9345,6 +5873,10 @@ bool ListEntitiesEventResponse::decode_length(uint32_t field_id, ProtoLengthDeli this->name = value.as_string(); return true; } + case 4: { + this->unique_id = value.as_string(); + return true; + } case 5: { this->icon = value.as_string(); return true; @@ -9375,6 +5907,7 @@ void ListEntitiesEventResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); buffer.encode_string(5, this->icon); buffer.encode_bool(6, this->disabled_by_default); buffer.encode_enum(7, this->entity_category); @@ -9382,11 +5915,13 @@ 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); ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_string_field(total_size, 1, this->unique_id, false); 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); @@ -9396,48 +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(" 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: { @@ -9461,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: { @@ -9503,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; } @@ -9517,6 +6015,10 @@ bool ListEntitiesValveResponse::decode_length(uint32_t field_id, ProtoLengthDeli this->name = value.as_string(); return true; } + case 4: { + this->unique_id = value.as_string(); + return true; + } case 5: { this->icon = value.as_string(); return true; @@ -9543,6 +6045,7 @@ void ListEntitiesValveResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); buffer.encode_string(5, this->icon); buffer.encode_bool(6, this->disabled_by_default); buffer.encode_enum(7, this->entity_category); @@ -9550,11 +6053,13 @@ 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); ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_string_field(total_size, 1, this->unique_id, false); 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); @@ -9562,60 +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(" 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; } @@ -9638,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: { @@ -9704,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: { @@ -9738,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; } @@ -9752,6 +6179,10 @@ bool ListEntitiesDateTimeResponse::decode_length(uint32_t field_id, ProtoLengthD this->name = value.as_string(); return true; } + case 4: { + this->unique_id = value.as_string(); + return true; + } case 5: { this->icon = value.as_string(); return true; @@ -9774,55 +6205,32 @@ void ListEntitiesDateTimeResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); 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); ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_string_field(total_size, 1, this->unique_id, false); 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(" 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; } @@ -9845,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: { @@ -9893,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: { @@ -9919,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; } @@ -9933,6 +6313,10 @@ bool ListEntitiesUpdateResponse::decode_length(uint32_t field_id, ProtoLengthDel this->name = value.as_string(); return true; } + case 4: { + this->unique_id = value.as_string(); + return true; + } case 5: { this->icon = value.as_string(); return true; @@ -9959,55 +6343,24 @@ void ListEntitiesUpdateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); buffer.encode_string(5, this->icon); 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); ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_string_field(total_size, 1, this->unique_id, false); 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_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(" 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: { @@ -10022,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; } @@ -10077,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); @@ -10089,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: { @@ -10166,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 5c1ef272d9..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, @@ -208,6 +229,7 @@ enum VoiceAssistantEvent : uint32_t { VOICE_ASSISTANT_STT_VAD_END = 12, VOICE_ASSISTANT_TTS_STREAM_START = 98, VOICE_ASSISTANT_TTS_STREAM_END = 99, + VOICE_ASSISTANT_INTENT_PROGRESS = 100, }; enum VoiceAssistantTimerEvent : uint32_t { VOICE_ASSISTANT_TIMER_STARTED = 0, @@ -215,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, @@ -236,29 +260,59 @@ 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 +class InfoResponseProtoMessage : public ProtoMessage { + public: + ~InfoResponseProtoMessage() override = default; + std::string object_id{}; + uint32_t key{0}; + std::string name{}; + std::string unique_id{}; + bool disabled_by_default{false}; + std::string icon{}; + enums::EntityCategory entity_category{}; + uint32_t device_id{0}; + + protected: +}; + +class StateResponseProtoMessage : public ProtoMessage { + public: + ~StateResponseProtoMessage() override = default; + uint32_t key{0}; + uint32_t device_id{0}; + + protected: +}; class HelloRequest : public ProtoMessage { public: 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}; @@ -278,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}; @@ -299,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; @@ -316,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; @@ -333,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 @@ -348,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 @@ -363,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 @@ -378,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 @@ -393,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 @@ -402,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{}; @@ -429,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 @@ -444,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 @@ -459,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 @@ -474,31 +546,24 @@ 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: }; -class ListEntitiesBinarySensorResponse : public ProtoMessage { +#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 object_id{}; - uint32_t key{0}; - std::string name{}; std::string device_class{}; bool is_status_binary_sensor{false}; - bool disabled_by_default{false}; - std::string icon{}; - enums::EntityCategory entity_category{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -510,14 +575,13 @@ class ListEntitiesBinarySensorResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class BinarySensorStateResponse : public ProtoMessage { +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 - uint32_t key{0}; bool state{false}; bool missing_state{false}; void encode(ProtoWriteBuffer buffer) const override; @@ -530,23 +594,19 @@ class BinarySensorStateResponse : public ProtoMessage { bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class ListEntitiesCoverResponse : public ProtoMessage { +#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 - std::string object_id{}; - uint32_t key{0}; - std::string name{}; bool assumed_state{false}; bool supports_position{false}; bool supports_tilt{false}; std::string device_class{}; - bool disabled_by_default{false}; - std::string icon{}; - enums::EntityCategory entity_category{}; bool supports_stop{false}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; @@ -559,14 +619,13 @@ class ListEntitiesCoverResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class CoverStateResponse : public ProtoMessage { +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 - uint32_t key{0}; enums::LegacyCoverState legacy_state{}; float position{0.0f}; float tilt{0.0f}; @@ -586,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}; @@ -606,23 +665,19 @@ class CoverCommandRequest : public ProtoMessage { bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class ListEntitiesFanResponse : public ProtoMessage { +#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 - std::string object_id{}; - uint32_t key{0}; - std::string name{}; bool supports_oscillation{false}; bool supports_speed{false}; bool supports_direction{false}; int32_t supported_speed_count{0}; - bool disabled_by_default{false}; - std::string icon{}; - enums::EntityCategory entity_category{}; std::vector supported_preset_modes{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; @@ -635,14 +690,13 @@ class ListEntitiesFanResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class FanStateResponse : public ProtoMessage { +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 - uint32_t key{0}; bool state{false}; bool oscillating{false}; enums::FanSpeed speed{}; @@ -665,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}; @@ -691,16 +745,15 @@ class FanCommandRequest : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class ListEntitiesLightResponse : public ProtoMessage { +#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::string object_id{}; - uint32_t key{0}; - std::string name{}; std::vector supported_color_modes{}; bool legacy_supports_brightness{false}; bool legacy_supports_rgb{false}; @@ -709,9 +762,6 @@ class ListEntitiesLightResponse : public ProtoMessage { float min_mireds{0.0f}; float max_mireds{0.0f}; std::vector effects{}; - bool disabled_by_default{false}; - std::string icon{}; - enums::EntityCategory entity_category{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -723,14 +773,13 @@ class ListEntitiesLightResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class LightStateResponse : public ProtoMessage { +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 - uint32_t key{0}; bool state{false}; float brightness{0.0f}; enums::ColorMode color_mode{}; @@ -759,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}; @@ -799,25 +848,21 @@ class LightCommandRequest : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class ListEntitiesSensorResponse : public ProtoMessage { +#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 object_id{}; - uint32_t key{0}; - std::string name{}; - std::string icon{}; std::string unit_of_measurement{}; int32_t accuracy_decimals{0}; bool force_update{false}; std::string device_class{}; enums::SensorStateClass state_class{}; enums::SensorLastResetType legacy_last_reset_type{}; - bool disabled_by_default{false}; - enums::EntityCategory entity_category{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -829,14 +874,13 @@ class ListEntitiesSensorResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class SensorStateResponse : public ProtoMessage { +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 - uint32_t key{0}; float state{0.0f}; bool missing_state{false}; void encode(ProtoWriteBuffer buffer) const override; @@ -849,20 +893,16 @@ class SensorStateResponse : public ProtoMessage { bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class ListEntitiesSwitchResponse : public ProtoMessage { +#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 - std::string object_id{}; - uint32_t key{0}; - std::string name{}; - std::string icon{}; bool assumed_state{false}; - bool disabled_by_default{false}; - enums::EntityCategory entity_category{}; std::string device_class{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; @@ -875,14 +915,13 @@ class ListEntitiesSwitchResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class SwitchStateResponse : public ProtoMessage { +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 - uint32_t key{0}; bool state{false}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; @@ -899,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}; @@ -913,19 +952,15 @@ class SwitchCommandRequest : public ProtoMessage { bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class ListEntitiesTextSensorResponse : public ProtoMessage { +#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 object_id{}; - uint32_t key{0}; - std::string name{}; - std::string icon{}; - bool disabled_by_default{false}; - enums::EntityCategory entity_category{}; std::string device_class{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; @@ -938,14 +973,13 @@ class ListEntitiesTextSensorResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class TextSensorStateResponse : public ProtoMessage { +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 - uint32_t key{0}; std::string state{}; bool missing_state{false}; void encode(ProtoWriteBuffer buffer) const override; @@ -959,12 +993,13 @@ class TextSensorStateResponse : public ProtoMessage { 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}; @@ -982,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{}; @@ -997,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; @@ -1019,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; @@ -1031,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 @@ -1064,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{}; @@ -1086,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 @@ -1101,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{}; @@ -1121,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{}; @@ -1140,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 @@ -1155,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; @@ -1186,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}; @@ -1228,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{}; @@ -1242,19 +1273,14 @@ class ExecuteServiceRequest : public ProtoMessage { bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; }; -class ListEntitiesCameraResponse : public ProtoMessage { +#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 - std::string object_id{}; - uint32_t key{0}; - std::string name{}; - bool disabled_by_default{false}; - std::string icon{}; - enums::EntityCategory entity_category{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1271,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{}; @@ -1292,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}; @@ -1305,16 +1331,15 @@ class CameraImageRequest : public ProtoMessage { protected: bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class ListEntitiesClimateResponse : public ProtoMessage { +#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 - std::string object_id{}; - uint32_t key{0}; - std::string name{}; bool supports_current_temperature{false}; bool supports_two_point_target_temperature{false}; std::vector supported_modes{}; @@ -1328,9 +1353,6 @@ class ListEntitiesClimateResponse : public ProtoMessage { std::vector supported_custom_fan_modes{}; std::vector supported_presets{}; std::vector supported_custom_presets{}; - bool disabled_by_default{false}; - std::string icon{}; - enums::EntityCategory entity_category{}; float visual_current_temperature_step{0.0f}; bool supports_current_humidity{false}; bool supports_target_humidity{false}; @@ -1347,14 +1369,13 @@ class ListEntitiesClimateResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class ClimateStateResponse : public ProtoMessage { +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 - uint32_t key{0}; enums::ClimateMode mode{}; float current_temperature{0.0f}; float target_temperature{0.0f}; @@ -1385,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}; @@ -1421,22 +1442,18 @@ class ClimateCommandRequest : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class ListEntitiesNumberResponse : public ProtoMessage { +#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 - std::string object_id{}; - uint32_t key{0}; - std::string name{}; - std::string icon{}; float min_value{0.0f}; float max_value{0.0f}; float step{0.0f}; - bool disabled_by_default{false}; - enums::EntityCategory entity_category{}; std::string unit_of_measurement{}; enums::NumberMode mode{}; std::string device_class{}; @@ -1451,14 +1468,13 @@ class ListEntitiesNumberResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class NumberStateResponse : public ProtoMessage { +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 - uint32_t key{0}; float state{0.0f}; bool missing_state{false}; void encode(ProtoWriteBuffer buffer) const override; @@ -1476,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}; @@ -1489,20 +1505,16 @@ class NumberCommandRequest : public ProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; }; -class ListEntitiesSelectResponse : public ProtoMessage { +#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::string object_id{}; - uint32_t key{0}; - std::string name{}; - std::string icon{}; std::vector options{}; - bool disabled_by_default{false}; - enums::EntityCategory entity_category{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1514,14 +1526,13 @@ class ListEntitiesSelectResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class SelectStateResponse : public ProtoMessage { +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 - uint32_t key{0}; std::string state{}; bool missing_state{false}; void encode(ProtoWriteBuffer buffer) const override; @@ -1540,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{}; @@ -1554,22 +1565,18 @@ class SelectCommandRequest : public ProtoMessage { bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; }; -class ListEntitiesSirenResponse : public ProtoMessage { +#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::string object_id{}; - uint32_t key{0}; - std::string name{}; - std::string icon{}; - bool disabled_by_default{false}; std::vector tones{}; bool supports_duration{false}; bool supports_volume{false}; - enums::EntityCategory entity_category{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1581,14 +1588,13 @@ class ListEntitiesSirenResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class SirenStateResponse : public ProtoMessage { +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 - uint32_t key{0}; bool state{false}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; @@ -1605,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}; @@ -1627,19 +1633,15 @@ class SirenCommandRequest : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class ListEntitiesLockResponse : public ProtoMessage { +#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 - std::string object_id{}; - uint32_t key{0}; - std::string name{}; - std::string icon{}; - bool disabled_by_default{false}; - enums::EntityCategory entity_category{}; bool assumed_state{false}; bool supports_open{false}; bool requires_code{false}; @@ -1655,14 +1657,13 @@ class ListEntitiesLockResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class LockStateResponse : public ProtoMessage { +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 - uint32_t key{0}; enums::LockState state{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; @@ -1679,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{}; @@ -1696,19 +1697,15 @@ class LockCommandRequest : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class ListEntitiesButtonResponse : public ProtoMessage { +#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 object_id{}; - uint32_t key{0}; - std::string name{}; - std::string icon{}; - bool disabled_by_default{false}; - enums::EntityCategory entity_category{}; std::string device_class{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; @@ -1726,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; @@ -1738,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{}; @@ -1755,19 +1754,13 @@ class MediaPlayerSupportedFormat : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class ListEntitiesMediaPlayerResponse : 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 - std::string object_id{}; - uint32_t key{0}; - std::string name{}; - std::string icon{}; - bool disabled_by_default{false}; - enums::EntityCategory entity_category{}; bool supports_pause{false}; std::vector supported_formats{}; void encode(ProtoWriteBuffer buffer) const override; @@ -1781,14 +1774,13 @@ class ListEntitiesMediaPlayerResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class MediaPlayerStateResponse : public ProtoMessage { +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 - uint32_t key{0}; enums::MediaPlayerState state{}; float volume{0.0f}; bool muted{false}; @@ -1807,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}; @@ -1829,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; @@ -1866,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{}; @@ -1906,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; @@ -1923,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{}; @@ -1943,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}; @@ -1963,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; @@ -2024,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{}; @@ -2043,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; @@ -2060,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}; @@ -2078,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}; @@ -2098,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}; @@ -2119,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}; @@ -2137,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}; @@ -2157,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}; @@ -2176,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}; @@ -2196,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 @@ -2211,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}; @@ -2230,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}; @@ -2249,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}; @@ -2267,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}; @@ -2285,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}; @@ -2304,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}; @@ -2323,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 @@ -2338,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}; @@ -2357,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{}; @@ -2375,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; @@ -2387,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}; @@ -2425,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{}; @@ -2447,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}; @@ -2478,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{}; @@ -2497,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}; @@ -2516,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{}; @@ -2539,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{}; @@ -2560,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; @@ -2591,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 @@ -2606,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{}; @@ -2626,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; @@ -2638,19 +2628,15 @@ class VoiceAssistantSetConfiguration : public ProtoMessage { protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; }; -class ListEntitiesAlarmControlPanelResponse : public ProtoMessage { +#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 - std::string object_id{}; - uint32_t key{0}; - std::string name{}; - std::string icon{}; - bool disabled_by_default{false}; - enums::EntityCategory entity_category{}; uint32_t supported_features{0}; bool requires_code{false}; bool requires_code_to_arm{false}; @@ -2665,14 +2651,13 @@ class ListEntitiesAlarmControlPanelResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class AlarmControlPanelStateResponse : public ProtoMessage { +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 - uint32_t key{0}; enums::AlarmControlPanelState state{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; @@ -2689,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{}; @@ -2705,19 +2690,15 @@ class AlarmControlPanelCommandRequest : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class ListEntitiesTextResponse : public ProtoMessage { +#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 - std::string object_id{}; - uint32_t key{0}; - std::string name{}; - std::string icon{}; - bool disabled_by_default{false}; - enums::EntityCategory entity_category{}; uint32_t min_length{0}; uint32_t max_length{0}; std::string pattern{}; @@ -2733,14 +2714,13 @@ class ListEntitiesTextResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class TextStateResponse : public ProtoMessage { +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 - uint32_t key{0}; std::string state{}; bool missing_state{false}; void encode(ProtoWriteBuffer buffer) const override; @@ -2759,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{}; @@ -2773,19 +2753,15 @@ class TextCommandRequest : public ProtoMessage { bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; }; -class ListEntitiesDateResponse : public ProtoMessage { +#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 - std::string object_id{}; - uint32_t key{0}; - std::string name{}; - std::string icon{}; - bool disabled_by_default{false}; - enums::EntityCategory entity_category{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -2797,14 +2773,13 @@ class ListEntitiesDateResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class DateStateResponse : public ProtoMessage { +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 - uint32_t key{0}; bool missing_state{false}; uint32_t year{0}; uint32_t month{0}; @@ -2824,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}; @@ -2840,19 +2815,15 @@ class DateCommandRequest : public ProtoMessage { bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class ListEntitiesTimeResponse : public ProtoMessage { +#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 - std::string object_id{}; - uint32_t key{0}; - std::string name{}; - std::string icon{}; - bool disabled_by_default{false}; - enums::EntityCategory entity_category{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -2864,14 +2835,13 @@ class ListEntitiesTimeResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class TimeStateResponse : public ProtoMessage { +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 - uint32_t key{0}; bool missing_state{false}; uint32_t hour{0}; uint32_t minute{0}; @@ -2891,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}; @@ -2907,19 +2877,15 @@ class TimeCommandRequest : public ProtoMessage { bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class ListEntitiesEventResponse : public ProtoMessage { +#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 object_id{}; - uint32_t key{0}; - std::string name{}; - std::string icon{}; - bool disabled_by_default{false}; - enums::EntityCategory entity_category{}; std::string device_class{}; std::vector event_types{}; void encode(ProtoWriteBuffer buffer) const override; @@ -2933,14 +2899,13 @@ class ListEntitiesEventResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class EventResponse : public ProtoMessage { +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 - uint32_t key{0}; std::string event_type{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; @@ -2951,20 +2916,17 @@ class EventResponse : public ProtoMessage { 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; }; -class ListEntitiesValveResponse : public ProtoMessage { +#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 object_id{}; - uint32_t key{0}; - std::string name{}; - std::string icon{}; - bool disabled_by_default{false}; - enums::EntityCategory entity_category{}; std::string device_class{}; bool assumed_state{false}; bool supports_position{false}; @@ -2980,14 +2942,13 @@ class ListEntitiesValveResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class ValveStateResponse : public ProtoMessage { +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 - uint32_t key{0}; float position{0.0f}; enums::ValveOperation current_operation{}; void encode(ProtoWriteBuffer buffer) const override; @@ -3005,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}; @@ -3021,19 +2982,15 @@ class ValveCommandRequest : public ProtoMessage { bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class ListEntitiesDateTimeResponse : public ProtoMessage { +#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 - std::string object_id{}; - uint32_t key{0}; - std::string name{}; - std::string icon{}; - bool disabled_by_default{false}; - enums::EntityCategory entity_category{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -3045,14 +3002,13 @@ class ListEntitiesDateTimeResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class DateTimeStateResponse : public ProtoMessage { +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 - uint32_t key{0}; bool missing_state{false}; uint32_t epoch_seconds{0}; void encode(ProtoWriteBuffer buffer) const override; @@ -3070,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}; @@ -3083,19 +3039,15 @@ class DateTimeCommandRequest : public ProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; }; -class ListEntitiesUpdateResponse : public ProtoMessage { +#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 object_id{}; - uint32_t key{0}; - std::string name{}; - std::string icon{}; - bool disabled_by_default{false}; - enums::EntityCategory entity_category{}; std::string device_class{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; @@ -3108,14 +3060,13 @@ class ListEntitiesUpdateResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class UpdateStateResponse : public ProtoMessage { +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 - uint32_t key{0}; bool missing_state{false}; bool in_progress{false}; bool has_progress{false}; @@ -3141,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{}; @@ -3155,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..d135294319 --- /dev/null +++ b/esphome/components/api/api_pb2_dump.cpp @@ -0,0 +1,4241 @@ +// 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: "); + 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("}"); +} +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("}"); +} +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: "); + sprintf(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: "); + sprintf(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: "); + sprintf(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: "); + 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"); + + 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: "); + 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(" 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: "); + sprintf(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: "); + 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(" device_id: "); + sprintf(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: "); + 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(" 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: "); + sprintf(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: "); + 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(" device_id: "); + sprintf(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: "); + 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 +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(" 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(" device_id: "); + sprintf(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: "); + 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(" device_id: "); + sprintf(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: "); + 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 +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"); + + 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(" device_id: "); + sprintf(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: "); + 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(" device_id: "); + sprintf(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: "); + 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 +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(" 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(" device_id: "); + sprintf(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: "); + 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(" device_id: "); + sprintf(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: "); + 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(" 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: "); + sprintf(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: "); + sprintf(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: "); + sprintf(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: "); + 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 +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(" 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: "); + sprintf(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: "); + 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(" device_id: "); + sprintf(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: "); + sprintf(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: "); + 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("}"); +} +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("}"); +} +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("}"); +} +#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: "); + 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(" 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: "); + sprintf(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: "); + sprintf(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: "); + 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(" 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(" device_id: "); + sprintf(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: "); + 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(" device_id: "); + sprintf(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: "); + 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 +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(" 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(" device_id: "); + sprintf(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: "); + 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(" device_id: "); + sprintf(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: "); + 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 +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(" 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: "); + sprintf(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: "); + 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(" device_id: "); + sprintf(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: "); + 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 +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(" 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: "); + sprintf(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: "); + sprintf(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: "); + sprintf(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: "); + 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 +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(" 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: "); + sprintf(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: "); + 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(" device_id: "); + sprintf(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: "); + 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 +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(" 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: "); + sprintf(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: "); + sprintf(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: "); + 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("}"); +} +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(" 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: "); + sprintf(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: "); + 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(" device_id: "); + sprintf(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: "); + 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 +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("}"); +} +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(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: "); + sprintf(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: "); + 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("}"); +} +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(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: "); + 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("}"); +} +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("}"); +} +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("}"); +} +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("}"); +} +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("}"); +} +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("}"); +} +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("}"); +} +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("}"); +} +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("}"); +} +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(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: "); + 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(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: "); + 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("}"); +} +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(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: "); + 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("}"); +} +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(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: "); + 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("}"); +} +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("}"); +} +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("}"); +} +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("}"); +} +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("}"); +} +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("}"); +} +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: "); + 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("}"); +} +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: "); + sprintf(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: "); + 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("}"); +} +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("}"); +} +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("}"); +} +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: "); + 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("}"); +} +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: "); + sprintf(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: "); + 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(" 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(" device_id: "); + sprintf(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: "); + 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(" device_id: "); + sprintf(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: "); + 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 +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(" 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(" device_id: "); + sprintf(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: "); + 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(" device_id: "); + sprintf(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: "); + 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 +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(" 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: "); + sprintf(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: "); + 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(" device_id: "); + sprintf(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: "); + 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 +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(" 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: "); + sprintf(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: "); + 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(" device_id: "); + sprintf(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: "); + 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 +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(" 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: "); + sprintf(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: "); + 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(" device_id: "); + sprintf(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: "); + 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(" 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: "); + sprintf(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: "); + 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(" device_id: "); + sprintf(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: "); + 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 +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(" 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: "); + sprintf(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: "); + 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(" device_id: "); + sprintf(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: "); + 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 +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(" 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: "); + sprintf(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: "); + 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(" device_id: "); + sprintf(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: "); + 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 +} // 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..575229cf04 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"); @@ -99,28 +112,35 @@ void APIServer::setup() { return; } for (auto &c : this->clients_) { - if (!c->remove_) + if (!c->flags_.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) { - for (auto &c : this->clients_) { - if (!c->remove_) - c->set_camera_state(image); - } - }); +#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() { // Accept new clients only if the socket exists and has incoming connections if (this->socket_ && this->socket_->ready()) { @@ -130,51 +150,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 +225,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,11 +256,12 @@ bool APIServer::check_password(const std::string &password) const { return result == 0; } +#endif void APIServer::handle_disconnect(APIConnection *conn) {} #ifdef USE_BINARY_SENSOR -void APIServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) { +void APIServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj) { if (obj->is_internal()) return; for (auto &c : this->clients_) @@ -406,9 +440,11 @@ 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 +515,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 +539,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 4edcc90f4a..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 @@ -29,8 +35,8 @@ async def async_run_logs(config: dict[str, Any], address: str) -> None: port: int = int(conf[CONF_PORT]) password: str = conf[CONF_PASSWORD] noise_psk: str | None = None - if CONF_ENCRYPTION in conf: - noise_psk = conf[CONF_ENCRYPTION][CONF_KEY] + if (encryption := conf.get(CONF_ENCRYPTION)) and (key := encryption.get(CONF_KEY)): + noise_psk = key _LOGGER.info("Starting log output from %s using esphome API", address) cli = APIClient( address, @@ -46,12 +52,10 @@ async def async_run_logs(config: dict[str, Any], address: str) -> None: time_ = datetime.now() message: bytes = msg.message text = message.decode("utf8", "backslashreplace") - if dashboard: - text = text.replace("\033", "\\033") for parsed_msg in parse_log_message( text, f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}]" ): - print(parsed_msg) + print(parsed_msg.replace("\033", "\\033") if dashboard else parsed_msg) stop = await async_run(cli, on_log, name=name) try: 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 5265c4520d..764bac2f39 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -216,7 +216,7 @@ class ProtoWriteBuffer { this->buffer_->insert(this->buffer_->end(), data, data + len); } void encode_string(uint32_t field_id, const std::string &value, bool force = false) { - this->encode_string(field_id, value.data(), value.size()); + this->encode_string(field_id, value.data(), value.size(), force); } void encode_bytes(uint32_t field_id, const uint8_t *data, size_t len, bool force = false) { this->encode_string(field_id, reinterpret_cast(data), len, force); @@ -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_reader.cpp b/esphome/components/audio/audio_reader.cpp index b82c6db9ee..6966c95db7 100644 --- a/esphome/components/audio/audio_reader.cpp +++ b/esphome/components/audio/audio_reader.cpp @@ -5,6 +5,7 @@ #include "esphome/core/defines.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" +#include "esphome/core/log.h" #if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE #include "esp_crt_bundle.h" @@ -16,13 +17,13 @@ namespace audio { static const uint32_t READ_WRITE_TIMEOUT_MS = 20; static const uint32_t CONNECTION_TIMEOUT_MS = 5000; - -// The number of times the http read times out with no data before throwing an error -static const uint32_t ERROR_COUNT_NO_DATA_READ_TIMEOUT = 100; +static const uint8_t MAX_FETCHING_HEADER_ATTEMPTS = 6; static const size_t HTTP_STREAM_BUFFER_SIZE = 2048; -static const uint8_t MAX_REDIRECTION = 5; +static const uint8_t MAX_REDIRECTIONS = 5; + +static const char *const TAG = "audio_reader"; // Some common HTTP status codes - borrowed from http_request component accessed 20241224 enum HttpStatus { @@ -94,7 +95,7 @@ esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) { client_config.url = uri.c_str(); client_config.cert_pem = nullptr; client_config.disable_auto_redirect = false; - client_config.max_redirection_count = 10; + client_config.max_redirection_count = MAX_REDIRECTIONS; client_config.event_handler = http_event_handler; client_config.user_data = this; client_config.buffer_size = HTTP_STREAM_BUFFER_SIZE; @@ -116,12 +117,29 @@ esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) { esp_err_t err = esp_http_client_open(this->client_, 0); if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to open URL"); this->cleanup_connection_(); return err; } int64_t header_length = esp_http_client_fetch_headers(this->client_); + uint8_t reattempt_count = 0; + while ((header_length < 0) && (reattempt_count < MAX_FETCHING_HEADER_ATTEMPTS)) { + this->cleanup_connection_(); + if (header_length != -ESP_ERR_HTTP_EAGAIN) { + // Serious error, no recovery + return ESP_FAIL; + } else { + // Reconnect from a fresh state to avoid a bug where it never reads the headers even if made available + this->client_ = esp_http_client_init(&client_config); + esp_http_client_open(this->client_, 0); + header_length = esp_http_client_fetch_headers(this->client_); + ++reattempt_count; + } + } + if (header_length < 0) { + ESP_LOGE(TAG, "Failed to fetch headers"); this->cleanup_connection_(); return ESP_FAIL; } @@ -135,7 +153,7 @@ esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) { ssize_t redirect_count = 0; - while ((esp_http_client_set_redirection(this->client_) == ESP_OK) && (redirect_count < MAX_REDIRECTION)) { + while ((esp_http_client_set_redirection(this->client_) == ESP_OK) && (redirect_count < MAX_REDIRECTIONS)) { err = esp_http_client_open(this->client_, 0); if (err != ESP_OK) { this->cleanup_connection_(); @@ -267,27 +285,29 @@ AudioReaderState AudioReader::http_read_() { return AudioReaderState::FINISHED; } } else if (this->output_transfer_buffer_->free() > 0) { - size_t bytes_to_read = this->output_transfer_buffer_->free(); - int received_len = - esp_http_client_read(this->client_, (char *) this->output_transfer_buffer_->get_buffer_end(), bytes_to_read); + int received_len = esp_http_client_read(this->client_, (char *) this->output_transfer_buffer_->get_buffer_end(), + this->output_transfer_buffer_->free()); if (received_len > 0) { this->output_transfer_buffer_->increase_buffer_length(received_len); this->last_data_read_ms_ = millis(); - } else if (received_len < 0) { + return AudioReaderState::READING; + } else if (received_len <= 0) { // HTTP read error - this->cleanup_connection_(); - return AudioReaderState::FAILED; - } else { - if (bytes_to_read > 0) { - // Read timed out - if ((millis() - this->last_data_read_ms_) > CONNECTION_TIMEOUT_MS) { - this->cleanup_connection_(); - return AudioReaderState::FAILED; - } - - delay(READ_WRITE_TIMEOUT_MS); + if (received_len == -1) { + // A true connection error occured, no chance at recovery + this->cleanup_connection_(); + return AudioReaderState::FAILED; } + + // Read timed out, manually verify if it has been too long since the last successful read + if ((millis() - this->last_data_read_ms_) > MAX_FETCHING_HEADER_ATTEMPTS * CONNECTION_TIMEOUT_MS) { + ESP_LOGE(TAG, "Timed out"); + this->cleanup_connection_(); + return AudioReaderState::FAILED; + } + + delay(READ_WRITE_TIMEOUT_MS); } } 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 7aeb818306..bf0adf1efd 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -58,7 +58,7 @@ static std::vector &get_batch_buffer() { return batch_buffer; } -bool BluetoothProxy::parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) { +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_) return false; @@ -73,7 +73,7 @@ bool BluetoothProxy::parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_p // Add new advertisements to the batch buffer for (size_t i = 0; i < count; i++) { - auto &result = advertisements[i]; + auto &result = scan_results[i]; uint8_t length = result.adv_data_len + result.scan_rsp_len; batch_buffer.emplace_back(); @@ -170,7 +170,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 f75e73e796..f0632350e0 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -52,7 +52,7 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com public: BluetoothProxy(); bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; - bool parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) override; + bool parse_devices(const esp32_ble::BLEScanResult *scan_results, size_t count) override; void dump_config() override; void setup() override; void loop() override; @@ -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/bme280_base/bme280_base.cpp b/esphome/components/bme280_base/bme280_base.cpp index 142a03fe1c..d2524e5aac 100644 --- a/esphome/components/bme280_base/bme280_base.cpp +++ b/esphome/components/bme280_base/bme280_base.cpp @@ -93,9 +93,8 @@ void BME280Component::setup() { // Mark as not failed before initializing. Some devices will turn off sensors to save on batteries // and when they come back on, the COMPONENT_STATE_FAILED bit must be unset on the component. - if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) { - this->component_state_ &= ~COMPONENT_STATE_MASK; - this->component_state_ |= COMPONENT_STATE_CONSTRUCTION; + if (this->is_failed()) { + this->reset_to_construction_state(); } if (!this->read_byte(BME280_REGISTER_CHIPID, &chip_id)) { 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..66a5fe5d81 100644 --- a/esphome/components/const/__init__.py +++ b/esphome/components/const/__init__.py @@ -3,4 +3,5 @@ CODEOWNERS = ["@esphome/core"] 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/datetime/date_entity.cpp b/esphome/components/datetime/date_entity.cpp index b5bcef43af..c164a98b2e 100644 --- a/esphome/components/datetime/date_entity.cpp +++ b/esphome/components/datetime/date_entity.cpp @@ -11,25 +11,25 @@ static const char *const TAG = "datetime.date_entity"; void DateEntity::publish_state() { if (this->year_ == 0 || this->month_ == 0 || this->day_ == 0) { - this->has_state_ = false; + this->set_has_state(false); return; } if (this->year_ < 1970 || this->year_ > 3000) { - this->has_state_ = false; + this->set_has_state(false); ESP_LOGE(TAG, "Year must be between 1970 and 3000"); return; } if (this->month_ < 1 || this->month_ > 12) { - this->has_state_ = false; + this->set_has_state(false); ESP_LOGE(TAG, "Month must be between 1 and 12"); return; } if (this->day_ > days_in_month(this->month_, this->year_)) { - this->has_state_ = false; + this->set_has_state(false); ESP_LOGE(TAG, "Day must be between 1 and %d for month %d", days_in_month(this->month_, this->year_), this->month_); return; } - this->has_state_ = true; + this->set_has_state(true); ESP_LOGD(TAG, "'%s': Sending date %d-%d-%d", this->get_name().c_str(), this->year_, this->month_, this->day_); this->state_callback_.call(); } diff --git a/esphome/components/datetime/datetime_base.h b/esphome/components/datetime/datetime_base.h index dea34e6110..b7645f5539 100644 --- a/esphome/components/datetime/datetime_base.h +++ b/esphome/components/datetime/datetime_base.h @@ -13,9 +13,6 @@ namespace datetime { class DateTimeBase : public EntityBase { public: - /// Return whether this Datetime has gotten a full state yet. - bool has_state() const { return this->has_state_; } - virtual ESPTime state_as_esptime() const = 0; void add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } @@ -31,8 +28,6 @@ class DateTimeBase : public EntityBase { #ifdef USE_TIME time::RealTimeClock *rtc_; #endif - - bool has_state_{false}; }; #ifdef USE_TIME diff --git a/esphome/components/datetime/datetime_entity.cpp b/esphome/components/datetime/datetime_entity.cpp index 3d92194efa..4e3b051eb3 100644 --- a/esphome/components/datetime/datetime_entity.cpp +++ b/esphome/components/datetime/datetime_entity.cpp @@ -11,40 +11,40 @@ static const char *const TAG = "datetime.datetime_entity"; void DateTimeEntity::publish_state() { if (this->year_ == 0 || this->month_ == 0 || this->day_ == 0) { - this->has_state_ = false; + this->set_has_state(false); return; } if (this->year_ < 1970 || this->year_ > 3000) { - this->has_state_ = false; + this->set_has_state(false); ESP_LOGE(TAG, "Year must be between 1970 and 3000"); return; } if (this->month_ < 1 || this->month_ > 12) { - this->has_state_ = false; + this->set_has_state(false); ESP_LOGE(TAG, "Month must be between 1 and 12"); return; } if (this->day_ > days_in_month(this->month_, this->year_)) { - this->has_state_ = false; + this->set_has_state(false); ESP_LOGE(TAG, "Day must be between 1 and %d for month %d", days_in_month(this->month_, this->year_), this->month_); return; } if (this->hour_ > 23) { - this->has_state_ = false; + this->set_has_state(false); ESP_LOGE(TAG, "Hour must be between 0 and 23"); return; } if (this->minute_ > 59) { - this->has_state_ = false; + this->set_has_state(false); ESP_LOGE(TAG, "Minute must be between 0 and 59"); return; } if (this->second_ > 59) { - this->has_state_ = false; + this->set_has_state(false); ESP_LOGE(TAG, "Second must be between 0 and 59"); return; } - this->has_state_ = true; + this->set_has_state(true); ESP_LOGD(TAG, "'%s': Sending datetime %04u-%02u-%02u %02d:%02d:%02d", this->get_name().c_str(), this->year_, this->month_, this->day_, this->hour_, this->minute_, this->second_); this->state_callback_.call(); diff --git a/esphome/components/datetime/time_entity.cpp b/esphome/components/datetime/time_entity.cpp index db0094ae01..9b05c2124f 100644 --- a/esphome/components/datetime/time_entity.cpp +++ b/esphome/components/datetime/time_entity.cpp @@ -11,21 +11,21 @@ static const char *const TAG = "datetime.time_entity"; void TimeEntity::publish_state() { if (this->hour_ > 23) { - this->has_state_ = false; + this->set_has_state(false); ESP_LOGE(TAG, "Hour must be between 0 and 23"); return; } if (this->minute_ > 59) { - this->has_state_ = false; + this->set_has_state(false); ESP_LOGE(TAG, "Minute must be between 0 and 59"); return; } if (this->second_ > 59) { - this->has_state_ = false; + this->set_has_state(false); ESP_LOGE(TAG, "Second must be between 0 and 59"); return; } - this->has_state_ = true; + this->set_has_state(true); ESP_LOGD(TAG, "'%s': Sending time %02d:%02d:%02d", this->get_name().c_str(), this->hour_, this->minute_, this->second_); this->state_callback_.call(); 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 b5d4c83f5e..b4c7a4e05b 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, @@ -94,6 +90,13 @@ COMPILER_OPTIMIZATIONS = { "SIZE": "CONFIG_COMPILER_OPTIMIZATION_SIZE", } +ARDUINO_ALLOWED_VARIANTS = [ + VARIANT_ESP32, + VARIANT_ESP32C3, + VARIANT_ESP32S2, + VARIANT_ESP32S3, +] + def get_cpu_frequencies(*frequencies): return [str(x) + "MHZ" for x in frequencies] @@ -125,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 @@ -143,12 +148,17 @@ def set_core_data(config): CORE.data[KEY_ESP32][KEY_COMPONENTS] = {} elif conf[CONF_TYPE] == FRAMEWORK_ARDUINO: CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "arduino" + if variant not in ARDUINO_ALLOWED_VARIANTS: + raise cv.Invalid( + f"ESPHome does not support using the Arduino framework for the {variant}. Please use the ESP-IDF framework instead.", + path=[CONF_FRAMEWORK, CONF_TYPE], + ) CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse( config[CONF_FRAMEWORK][CONF_VERSION] ) CORE.data[KEY_ESP32][KEY_BOARD] = config[CONF_BOARD] - CORE.data[KEY_ESP32][KEY_VARIANT] = config[CONF_VARIANT] + CORE.data[KEY_ESP32][KEY_VARIANT] = variant CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES] = {} return config @@ -221,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, @@ -231,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): @@ -277,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( @@ -305,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 @@ -339,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), @@ -353,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), } @@ -376,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. " @@ -404,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 @@ -415,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}'" @@ -562,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( { @@ -593,7 +606,7 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All( CONF_ENABLE_LWIP_DHCP_SERVER, "wifi", default=False ): cv.boolean, cv.Optional( - CONF_ENABLE_LWIP_MDNS_QUERIES, default=False + CONF_ENABLE_LWIP_MDNS_QUERIES, default=True ): cv.boolean, cv.Optional( CONF_ENABLE_LWIP_BRIDGE_INTERFACE, default=False @@ -601,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, ) ), } @@ -618,6 +635,21 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All( ) +def _set_default_framework(config): + if CONF_FRAMEWORK not in config: + config = config.copy() + + variant = config[CONF_VARIANT] + if variant in ARDUINO_ALLOWED_VARIANTS: + config[CONF_FRAMEWORK] = ARDUINO_FRAMEWORK_SCHEMA({}) + config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ARDUINO + else: + config[CONF_FRAMEWORK] = ESP_IDF_FRAMEWORK_SCHEMA({}) + config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ESP_IDF + + return config + + FRAMEWORK_ESP_IDF = "esp-idf" FRAMEWORK_ARDUINO = "arduino" FRAMEWORK_SCHEMA = cv.typed_schema( @@ -627,7 +659,6 @@ FRAMEWORK_SCHEMA = cv.typed_schema( }, lower=True, space="-", - default_type=FRAMEWORK_ARDUINO, ) @@ -654,10 +685,11 @@ CONFIG_SCHEMA = cv.All( ), cv.Optional(CONF_PARTITIONS): cv.file_, cv.Optional(CONF_VARIANT): cv.one_of(*VARIANTS, upper=True), - cv.Optional(CONF_FRAMEWORK, default={}): FRAMEWORK_SCHEMA, + cv.Optional(CONF_FRAMEWORK): FRAMEWORK_SCHEMA, } ), _detect_variant, + _set_default_framework, set_core_data, ) @@ -668,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]}") @@ -721,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) @@ -733,7 +769,7 @@ async def to_code(config): and not advanced[CONF_ENABLE_LWIP_DHCP_SERVER] ): add_idf_sdkconfig_option("CONFIG_LWIP_DHCPS", False) - if not advanced.get(CONF_ENABLE_LWIP_MDNS_QUERIES, False): + if not advanced.get(CONF_ENABLE_LWIP_MDNS_QUERIES, True): add_idf_sdkconfig_option("CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES", False) if not advanced.get(CONF_ENABLE_LWIP_BRIDGE_INTERFACE, False): add_idf_sdkconfig_option("CONFIG_LWIP_BRIDGEIF_MAX_PORTS", 0) @@ -760,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." @@ -785,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]) @@ -898,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: @@ -910,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"), @@ -926,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 824c2b9dbc..8b0cf4da98 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -3,6 +3,7 @@ #include "ble.h" #include "esphome/core/application.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #include @@ -23,9 +24,6 @@ namespace esp32_ble { static const char *const TAG = "esp32_ble"; -static RAMAllocator EVENT_ALLOCATOR( // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - RAMAllocator::ALLOW_FAILURE | RAMAllocator::ALLOC_INTERNAL); - void ESP32BLE::setup() { global_ble = this; ESP_LOGCONFIG(TAG, "Running setup"); @@ -304,82 +302,191 @@ void ESP32BLE::loop() { BLEEvent *ble_event = this->ble_events_.pop(); while (ble_event != nullptr) { switch (ble_event->type_) { - case BLEEvent::GATTS: - this->real_gatts_event_handler_(ble_event->event_.gatts.gatts_event, ble_event->event_.gatts.gatts_if, - &ble_event->event_.gatts.gatts_param); + case BLEEvent::GATTS: { + esp_gatts_cb_event_t event = ble_event->event_.gatts.gatts_event; + esp_gatt_if_t gatts_if = ble_event->event_.gatts.gatts_if; + esp_ble_gatts_cb_param_t *param = ble_event->event_.gatts.gatts_param; + ESP_LOGV(TAG, "gatts_event [esp_gatt_if: %d] - %d", gatts_if, event); + for (auto *gatts_handler : this->gatts_event_handlers_) { + gatts_handler->gatts_event_handler(event, gatts_if, param); + } break; - case BLEEvent::GATTC: - this->real_gattc_event_handler_(ble_event->event_.gattc.gattc_event, ble_event->event_.gattc.gattc_if, - &ble_event->event_.gattc.gattc_param); + } + case BLEEvent::GATTC: { + esp_gattc_cb_event_t event = ble_event->event_.gattc.gattc_event; + esp_gatt_if_t gattc_if = ble_event->event_.gattc.gattc_if; + esp_ble_gattc_cb_param_t *param = ble_event->event_.gattc.gattc_param; + ESP_LOGV(TAG, "gattc_event [esp_gatt_if: %d] - %d", gattc_if, event); + for (auto *gattc_handler : this->gattc_event_handlers_) { + gattc_handler->gattc_event_handler(event, gattc_if, param); + } break; - case BLEEvent::GAP: - this->real_gap_event_handler_(ble_event->event_.gap.gap_event, &ble_event->event_.gap.gap_param); + } + case BLEEvent::GAP: { + esp_gap_ble_cb_event_t gap_event = ble_event->event_.gap.gap_event; + switch (gap_event) { + case ESP_GAP_BLE_SCAN_RESULT_EVT: + // Use the new scan event handler - no memcpy! + for (auto *scan_handler : this->gap_scan_event_handlers_) { + scan_handler->gap_scan_event_handler(ble_event->scan_result()); + } + break; + + // Scan complete events + case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: + case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: + case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: + // All three scan complete events have the same structure with just status + // The scan_complete struct matches ESP-IDF's layout exactly, so this reinterpret_cast is safe + // This is verified at compile-time by static_assert checks in ble_event.h + // The struct already contains our copy of the status (copied in BLEEvent constructor) + ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); + for (auto *gap_handler : this->gap_event_handlers_) { + gap_handler->gap_event_handler( + gap_event, reinterpret_cast(&ble_event->event_.gap.scan_complete)); + } + break; + + // Advertising complete events + case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT: + case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT: + case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: + case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: + case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT: + // All advertising complete events have the same structure with just status + ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); + for (auto *gap_handler : this->gap_event_handlers_) { + gap_handler->gap_event_handler( + gap_event, reinterpret_cast(&ble_event->event_.gap.adv_complete)); + } + break; + + // RSSI complete event + case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT: + ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); + for (auto *gap_handler : this->gap_event_handlers_) { + gap_handler->gap_event_handler( + gap_event, reinterpret_cast(&ble_event->event_.gap.read_rssi_complete)); + } + break; + + // Security events + case ESP_GAP_BLE_AUTH_CMPL_EVT: + case ESP_GAP_BLE_SEC_REQ_EVT: + case ESP_GAP_BLE_PASSKEY_NOTIF_EVT: + case ESP_GAP_BLE_PASSKEY_REQ_EVT: + case ESP_GAP_BLE_NC_REQ_EVT: + ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); + for (auto *gap_handler : this->gap_event_handlers_) { + gap_handler->gap_event_handler( + gap_event, reinterpret_cast(&ble_event->event_.gap.security)); + } + break; + + default: + // Unknown/unhandled event + ESP_LOGW(TAG, "Unhandled GAP event type in loop: %d", gap_event); + break; + } break; + } default: break; } - ble_event->~BLEEvent(); - EVENT_ALLOCATOR.deallocate(ble_event, 1); + // Return the event to the pool + this->ble_event_pool_.release(ble_event); ble_event = this->ble_events_.pop(); } if (this->advertising_ != nullptr) { this->advertising_->loop(); } + + // Log dropped events periodically + uint16_t dropped = this->ble_events_.get_and_reset_dropped_count(); + if (dropped > 0) { + ESP_LOGW(TAG, "Dropped %u BLE events due to buffer overflow", dropped); + } } -void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { - BLEEvent *new_event = EVENT_ALLOCATOR.allocate(1); - if (new_event == nullptr) { - // Memory too fragmented to allocate new event. Can only drop it until memory comes back +// Helper function to load new event data based on type +void load_ble_event(BLEEvent *event, esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { + event->load_gap_event(e, p); +} + +void load_ble_event(BLEEvent *event, esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) { + event->load_gattc_event(e, i, p); +} + +void load_ble_event(BLEEvent *event, esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) { + event->load_gatts_event(e, i, p); +} + +template void enqueue_ble_event(Args... args) { + // Allocate an event from the pool + BLEEvent *event = global_ble->ble_event_pool_.allocate(); + if (event == nullptr) { + // No events available - queue is full or we're out of memory + global_ble->ble_events_.increment_dropped_count(); return; } - new (new_event) BLEEvent(event, param); - global_ble->ble_events_.push(new_event); -} // NOLINT(clang-analyzer-unix.Malloc) -void ESP32BLE::real_gap_event_handler_(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { - ESP_LOGV(TAG, "(BLE) gap_event_handler - %d", event); - for (auto *gap_handler : this->gap_event_handlers_) { - gap_handler->gap_event_handler(event, param); + // Load new event data (replaces previous event) + load_ble_event(event, args...); + + // Push the event to the queue + global_ble->ble_events_.push(event); + // Push always succeeds because we're the only producer and the pool ensures we never exceed queue size +} + +// Explicit template instantiations for the friend function +template void enqueue_ble_event(esp_gap_ble_cb_event_t, esp_ble_gap_cb_param_t *); +template void enqueue_ble_event(esp_gatts_cb_event_t, esp_gatt_if_t, esp_ble_gatts_cb_param_t *); +template void enqueue_ble_event(esp_gattc_cb_event_t, esp_gatt_if_t, esp_ble_gattc_cb_param_t *); + +void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { + switch (event) { + // Queue GAP events that components need to handle + // Scanning events - used by esp32_ble_tracker + case ESP_GAP_BLE_SCAN_RESULT_EVT: + case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: + case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: + case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: + // Advertising events - used by esp32_ble_beacon and esp32_ble server + case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT: + case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT: + case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: + case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: + case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT: + // Connection events - used by ble_client + case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT: + // Security events - used by ble_client and bluetooth_proxy + case ESP_GAP_BLE_AUTH_CMPL_EVT: + case ESP_GAP_BLE_SEC_REQ_EVT: + case ESP_GAP_BLE_PASSKEY_NOTIF_EVT: + case ESP_GAP_BLE_PASSKEY_REQ_EVT: + case ESP_GAP_BLE_NC_REQ_EVT: + enqueue_ble_event(event, param); + return; + + // Ignore these GAP events as they are not relevant for our use case + case ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT: + case ESP_GAP_BLE_SET_PKT_LENGTH_COMPLETE_EVT: + return; + + default: + break; } + ESP_LOGW(TAG, "Ignoring unexpected GAP event type: %d", event); } void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) { - BLEEvent *new_event = EVENT_ALLOCATOR.allocate(1); - if (new_event == nullptr) { - // Memory too fragmented to allocate new event. Can only drop it until memory comes back - return; - } - new (new_event) BLEEvent(event, gatts_if, param); - global_ble->ble_events_.push(new_event); -} // NOLINT(clang-analyzer-unix.Malloc) - -void ESP32BLE::real_gatts_event_handler_(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, - esp_ble_gatts_cb_param_t *param) { - ESP_LOGV(TAG, "(BLE) gatts_event [esp_gatt_if: %d] - %d", gatts_if, event); - for (auto *gatts_handler : this->gatts_event_handlers_) { - gatts_handler->gatts_event_handler(event, gatts_if, param); - } + enqueue_ble_event(event, gatts_if, param); } void ESP32BLE::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { - BLEEvent *new_event = EVENT_ALLOCATOR.allocate(1); - if (new_event == nullptr) { - // Memory too fragmented to allocate new event. Can only drop it until memory comes back - return; - } - new (new_event) BLEEvent(event, gattc_if, param); - global_ble->ble_events_.push(new_event); -} // NOLINT(clang-analyzer-unix.Malloc) - -void ESP32BLE::real_gattc_event_handler_(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, - esp_ble_gattc_cb_param_t *param) { - ESP_LOGV(TAG, "(BLE) gattc_event [esp_gatt_if: %d] - %d", gattc_if, event); - for (auto *gattc_handler : this->gattc_event_handlers_) { - gattc_handler->gattc_event_handler(event, gattc_if, param); - } + enqueue_ble_event(event, gattc_if, param); } float ESP32BLE::get_setup_priority() const { return setup_priority::BLUETOOTH; } @@ -409,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 13ec3b6dd9..81582eb09a 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -2,6 +2,7 @@ #include "ble_advertising.h" #include "ble_uuid.h" +#include "ble_scan_result.h" #include @@ -11,7 +12,8 @@ #include "esphome/core/helpers.h" #include "ble_event.h" -#include "queue.h" +#include "esphome/core/lock_free_queue.h" +#include "esphome/core/event_pool.h" #ifdef USE_ESP32 @@ -22,6 +24,16 @@ namespace esphome { namespace esp32_ble { +// Maximum number of BLE scan results to buffer +#ifdef USE_PSRAM +static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 32; +#else +static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 20; +#endif + +// Maximum size of the BLE event queue - must be power of 2 for lock-free queue +static constexpr size_t MAX_BLE_QUEUE_SIZE = 64; + uint64_t ble_addr_to_uint64(const esp_bd_addr_t address); // NOLINTNEXTLINE(modernize-use-using) @@ -39,7 +51,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. */ @@ -57,6 +69,11 @@ class GAPEventHandler { virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) = 0; }; +class GAPScanEventHandler { + public: + virtual void gap_scan_event_handler(const BLEScanResult &scan_result) = 0; +}; + class GATTcEventHandler { public: virtual void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, @@ -101,6 +118,9 @@ class ESP32BLE : public Component { void advertising_register_raw_advertisement_callback(std::function &&callback); void register_gap_event_handler(GAPEventHandler *handler) { this->gap_event_handlers_.push_back(handler); } + void register_gap_scan_event_handler(GAPScanEventHandler *handler) { + this->gap_scan_event_handlers_.push_back(handler); + } void register_gattc_event_handler(GATTcEventHandler *handler) { this->gattc_event_handlers_.push_back(handler); } void register_gatts_event_handler(GATTsEventHandler *handler) { this->gatts_event_handlers_.push_back(handler); } void register_ble_status_event_handler(BLEStatusEventHandler *handler) { @@ -113,28 +133,39 @@ class ESP32BLE : public Component { static void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param); static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); - void real_gatts_event_handler_(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); - void real_gattc_event_handler_(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param); - void real_gap_event_handler_(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); - bool ble_setup_(); bool ble_dismantle_(); bool ble_pre_setup_(); void advertising_init_(); + 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}; - Queue ble_events_; - 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 1cf63b2fab..9268c710f3 100644 --- a/esphome/components/esp32_ble/ble_event.h +++ b/esphome/components/esp32_ble/ble_event.h @@ -2,92 +2,399 @@ #ifdef USE_ESP32 +#include // for offsetof #include #include #include #include +#include "ble_scan_result.h" + namespace esphome { namespace esp32_ble { + +// Compile-time verification that ESP-IDF scan complete events only contain a status field +// This ensures our reinterpret_cast in ble.cpp is safe +static_assert(sizeof(esp_ble_gap_cb_param_t::ble_scan_param_cmpl_evt_param) == sizeof(esp_bt_status_t), + "ESP-IDF scan_param_cmpl structure has unexpected size"); +static_assert(sizeof(esp_ble_gap_cb_param_t::ble_scan_start_cmpl_evt_param) == sizeof(esp_bt_status_t), + "ESP-IDF scan_start_cmpl structure has unexpected size"); +static_assert(sizeof(esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param) == sizeof(esp_bt_status_t), + "ESP-IDF scan_stop_cmpl structure has unexpected size"); + +// Verify the status field is at offset 0 (first member) +static_assert(offsetof(esp_ble_gap_cb_param_t, scan_param_cmpl.status) == 0, + "status must be first member of scan_param_cmpl"); +static_assert(offsetof(esp_ble_gap_cb_param_t, scan_start_cmpl.status) == 0, + "status must be first member of scan_start_cmpl"); +static_assert(offsetof(esp_ble_gap_cb_param_t, scan_stop_cmpl.status) == 0, + "status must be first member of scan_stop_cmpl"); + +// Compile-time verification for advertising complete events +static_assert(sizeof(esp_ble_gap_cb_param_t::ble_adv_data_cmpl_evt_param) == sizeof(esp_bt_status_t), + "ESP-IDF adv_data_cmpl structure has unexpected size"); +static_assert(sizeof(esp_ble_gap_cb_param_t::ble_scan_rsp_data_cmpl_evt_param) == sizeof(esp_bt_status_t), + "ESP-IDF scan_rsp_data_cmpl structure has unexpected size"); +static_assert(sizeof(esp_ble_gap_cb_param_t::ble_adv_data_raw_cmpl_evt_param) == sizeof(esp_bt_status_t), + "ESP-IDF adv_data_raw_cmpl structure has unexpected size"); +static_assert(sizeof(esp_ble_gap_cb_param_t::ble_adv_start_cmpl_evt_param) == sizeof(esp_bt_status_t), + "ESP-IDF adv_start_cmpl structure has unexpected size"); +static_assert(sizeof(esp_ble_gap_cb_param_t::ble_adv_stop_cmpl_evt_param) == sizeof(esp_bt_status_t), + "ESP-IDF adv_stop_cmpl structure has unexpected size"); + +// Verify the status field is at offset 0 for advertising events +static_assert(offsetof(esp_ble_gap_cb_param_t, adv_data_cmpl.status) == 0, + "status must be first member of adv_data_cmpl"); +static_assert(offsetof(esp_ble_gap_cb_param_t, scan_rsp_data_cmpl.status) == 0, + "status must be first member of scan_rsp_data_cmpl"); +static_assert(offsetof(esp_ble_gap_cb_param_t, adv_data_raw_cmpl.status) == 0, + "status must be first member of adv_data_raw_cmpl"); +static_assert(offsetof(esp_ble_gap_cb_param_t, adv_start_cmpl.status) == 0, + "status must be first member of adv_start_cmpl"); +static_assert(offsetof(esp_ble_gap_cb_param_t, adv_stop_cmpl.status) == 0, + "status must be first member of adv_stop_cmpl"); + +// Compile-time verification for RSSI complete event structure +static_assert(offsetof(esp_ble_gap_cb_param_t, read_rssi_cmpl.status) == 0, + "status must be first member of read_rssi_cmpl"); +static_assert(offsetof(esp_ble_gap_cb_param_t, read_rssi_cmpl.rssi) == sizeof(esp_bt_status_t), + "rssi must immediately follow status in read_rssi_cmpl"); +static_assert(offsetof(esp_ble_gap_cb_param_t, read_rssi_cmpl.remote_addr) == sizeof(esp_bt_status_t) + sizeof(int8_t), + "remote_addr must follow rssi in read_rssi_cmpl"); + // Received GAP, GATTC and GATTS events are only queued, and get processed in the main loop(). -// This class stores each event in a single type. +// This class stores each event with minimal memory usage. +// GAP events (99% of traffic) don't have the vector overhead. +// GATTC/GATTS events use heap allocation for their param and data. +// +// Event flow: +// 1. ESP-IDF BLE stack calls our static handlers in the BLE task context +// 2. The handlers create a BLEEvent instance, copying only the data we need +// 3. The event is pushed to a thread-safe queue +// 4. In the main loop(), events are popped from the queue and processed +// 5. The event destructor cleans up any external allocations +// +// Thread safety: +// - GAP events: We copy only the fields we need directly into the union +// - GATTC/GATTS events: We heap-allocate and copy the entire param struct, ensuring +// the data remains valid even after the BLE callback returns. The original +// param pointer from ESP-IDF is only valid during the callback. +// +// CRITICAL DESIGN NOTE: +// The heap allocations for GATTC/GATTS events are REQUIRED for memory safety. +// DO NOT attempt to optimize by removing these allocations or storing pointers +// to the original ESP-IDF data. The ESP-IDF callback data has a different lifetime +// than our event processing, and accessing it after the callback returns would +// result in use-after-free bugs and crashes. class BLEEvent { public: - BLEEvent(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { - this->event_.gap.gap_event = e; - memcpy(&this->event_.gap.gap_param, p, sizeof(esp_ble_gap_cb_param_t)); - this->type_ = GAP; - }; - - BLEEvent(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) { - this->event_.gattc.gattc_event = e; - this->event_.gattc.gattc_if = i; - memcpy(&this->event_.gattc.gattc_param, p, sizeof(esp_ble_gattc_cb_param_t)); - // Need to also make a copy of relevant event data. - switch (e) { - case ESP_GATTC_NOTIFY_EVT: - this->data.assign(p->notify.value, p->notify.value + p->notify.value_len); - this->event_.gattc.gattc_param.notify.value = this->data.data(); - break; - case ESP_GATTC_READ_CHAR_EVT: - case ESP_GATTC_READ_DESCR_EVT: - this->data.assign(p->read.value, p->read.value + p->read.value_len); - this->event_.gattc.gattc_param.read.value = this->data.data(); - break; - default: - break; - } - this->type_ = GATTC; - }; - - BLEEvent(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) { - this->event_.gatts.gatts_event = e; - this->event_.gatts.gatts_if = i; - memcpy(&this->event_.gatts.gatts_param, p, sizeof(esp_ble_gatts_cb_param_t)); - // Need to also make a copy of relevant event data. - switch (e) { - case ESP_GATTS_WRITE_EVT: - this->data.assign(p->write.value, p->write.value + p->write.len); - this->event_.gatts.gatts_param.write.value = this->data.data(); - break; - default: - break; - } - this->type_ = GATTS; - }; - - union { - // NOLINTNEXTLINE(readability-identifier-naming) - struct gap_event { - esp_gap_ble_cb_event_t gap_event; - esp_ble_gap_cb_param_t gap_param; - } gap; - - // NOLINTNEXTLINE(readability-identifier-naming) - struct gattc_event { - esp_gattc_cb_event_t gattc_event; - esp_gatt_if_t gattc_if; - esp_ble_gattc_cb_param_t gattc_param; - } gattc; - - // NOLINTNEXTLINE(readability-identifier-naming) - struct gatts_event { - esp_gatts_cb_event_t gatts_event; - esp_gatt_if_t gatts_if; - esp_ble_gatts_cb_param_t gatts_param; - } gatts; - } event_; - - std::vector data{}; // NOLINTNEXTLINE(readability-identifier-naming) enum ble_event_t : uint8_t { GAP, GATTC, GATTS, - } type_; + }; + + // Type definitions for cleaner method signatures + struct StatusOnlyData { + esp_bt_status_t status; + }; + + struct RSSICompleteData { + esp_bt_status_t status; + int8_t rssi; + esp_bd_addr_t remote_addr; + }; + + // Constructor for GAP events - no external allocations needed + BLEEvent(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { + this->type_ = GAP; + this->init_gap_data_(e, p); + } + + // Constructor for GATTC events - uses heap allocation + // IMPORTANT: The heap allocation is REQUIRED and must not be removed as an optimization. + // The param pointer from ESP-IDF is only valid during the callback execution. + // Since BLE events are processed asynchronously in the main loop, we must create + // our own copy to ensure the data remains valid until the event is processed. + BLEEvent(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) { + this->type_ = GATTC; + this->init_gattc_data_(e, i, p); + } + + // Constructor for GATTS events - uses heap allocation + // IMPORTANT: The heap allocation is REQUIRED and must not be removed as an optimization. + // The param pointer from ESP-IDF is only valid during the callback execution. + // Since BLE events are processed asynchronously in the main loop, we must create + // our own copy to ensure the data remains valid until the event is processed. + BLEEvent(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) { + this->type_ = GATTS; + this->init_gatts_data_(e, i, p); + } + + // Destructor to clean up heap allocations + ~BLEEvent() { this->release(); } + + // Default constructor for pre-allocation in pool + BLEEvent() : type_(GAP) {} + + // Invoked on return to EventPool - clean up any heap-allocated data + void release() { + if (this->type_ == GAP) { + return; + } + if (this->type_ == GATTC) { + delete this->event_.gattc.gattc_param; + delete this->event_.gattc.data; + this->event_.gattc.gattc_param = nullptr; + this->event_.gattc.data = nullptr; + return; + } + if (this->type_ == GATTS) { + delete this->event_.gatts.gatts_param; + delete this->event_.gatts.data; + this->event_.gatts.gatts_param = nullptr; + this->event_.gatts.data = nullptr; + } + } + + // 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->release(); + this->type_ = GAP; + this->init_gap_data_(e, p); + } + + void load_gattc_event(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) { + this->release(); + this->type_ = GATTC; + this->init_gattc_data_(e, i, p); + } + + void load_gatts_event(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) { + this->release(); + this->type_ = GATTS; + this->init_gatts_data_(e, i, p); + } + + // Disable copy to prevent double-delete + BLEEvent(const BLEEvent &) = delete; + BLEEvent &operator=(const BLEEvent &) = delete; + + union { + // NOLINTNEXTLINE(readability-identifier-naming) + struct gap_event { + esp_gap_ble_cb_event_t gap_event; + union { + BLEScanResult scan_result; // 73 bytes - Used by: esp32_ble_tracker + // This matches ESP-IDF's scan complete event structures + // All three (scan_param_cmpl, scan_start_cmpl, scan_stop_cmpl) have identical layout + // Used by: esp32_ble_tracker + StatusOnlyData scan_complete; // 1 byte + // Advertising complete events all have same structure + // Used by: esp32_ble_beacon, esp32_ble server components + // ADV_DATA_SET, SCAN_RSP_DATA_SET, ADV_DATA_RAW_SET, ADV_START, ADV_STOP + StatusOnlyData adv_complete; // 1 byte + // RSSI complete event + // Used by: ble_client (ble_rssi_sensor component) + RSSICompleteData read_rssi_complete; // 8 bytes + // Security events - we store the full security union + // Used by: ble_client (automation), bluetooth_proxy, esp32_ble_client + esp_ble_sec_t security; // Variable size, but fits within scan_result size + }; + } gap; // 80 bytes total + + // NOLINTNEXTLINE(readability-identifier-naming) + struct gattc_event { + esp_gattc_cb_event_t gattc_event; + esp_gatt_if_t gattc_if; + esp_ble_gattc_cb_param_t *gattc_param; // Heap-allocated + std::vector *data; // Heap-allocated + } gattc; // 16 bytes (pointers only) + + // NOLINTNEXTLINE(readability-identifier-naming) + struct gatts_event { + esp_gatts_cb_event_t gatts_event; + esp_gatt_if_t gatts_if; + esp_ble_gatts_cb_param_t *gatts_param; // Heap-allocated + std::vector *data; // Heap-allocated + } gatts; // 16 bytes (pointers only) + } event_; // 80 bytes + + ble_event_t type_; + + // Helper methods to access event data + ble_event_t type() const { return type_; } + esp_gap_ble_cb_event_t gap_event_type() const { return event_.gap.gap_event; } + const BLEScanResult &scan_result() const { return event_.gap.scan_result; } + esp_bt_status_t scan_complete_status() const { return event_.gap.scan_complete.status; } + esp_bt_status_t adv_complete_status() const { return event_.gap.adv_complete.status; } + const RSSICompleteData &read_rssi_complete() const { return event_.gap.read_rssi_complete; } + const esp_ble_sec_t &security() const { return event_.gap.security; } + + private: + // Initialize GAP event data + void init_gap_data_(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { + this->event_.gap.gap_event = e; + + if (p == nullptr) { + return; // Invalid event, but we can't log in header file + } + + // Copy data based on event type + switch (e) { + case ESP_GAP_BLE_SCAN_RESULT_EVT: + memcpy(this->event_.gap.scan_result.bda, p->scan_rst.bda, sizeof(esp_bd_addr_t)); + this->event_.gap.scan_result.ble_addr_type = p->scan_rst.ble_addr_type; + this->event_.gap.scan_result.rssi = p->scan_rst.rssi; + this->event_.gap.scan_result.adv_data_len = p->scan_rst.adv_data_len; + this->event_.gap.scan_result.scan_rsp_len = p->scan_rst.scan_rsp_len; + this->event_.gap.scan_result.search_evt = p->scan_rst.search_evt; + memcpy(this->event_.gap.scan_result.ble_adv, p->scan_rst.ble_adv, + ESP_BLE_ADV_DATA_LEN_MAX + ESP_BLE_SCAN_RSP_DATA_LEN_MAX); + break; + + case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: + this->event_.gap.scan_complete.status = p->scan_param_cmpl.status; + break; + + case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: + this->event_.gap.scan_complete.status = p->scan_start_cmpl.status; + break; + + case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: + this->event_.gap.scan_complete.status = p->scan_stop_cmpl.status; + break; + + // Advertising complete events - all have same structure with just status + // Used by: esp32_ble_beacon, esp32_ble server components + case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT: + this->event_.gap.adv_complete.status = p->adv_data_cmpl.status; + break; + case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT: + this->event_.gap.adv_complete.status = p->scan_rsp_data_cmpl.status; + break; + case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: // Used by: esp32_ble_beacon + this->event_.gap.adv_complete.status = p->adv_data_raw_cmpl.status; + break; + case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: // Used by: esp32_ble_beacon + this->event_.gap.adv_complete.status = p->adv_start_cmpl.status; + break; + case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT: // Used by: esp32_ble_beacon + this->event_.gap.adv_complete.status = p->adv_stop_cmpl.status; + break; + + // RSSI complete event + // Used by: ble_client (ble_rssi_sensor) + case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT: + this->event_.gap.read_rssi_complete.status = p->read_rssi_cmpl.status; + this->event_.gap.read_rssi_complete.rssi = p->read_rssi_cmpl.rssi; + memcpy(this->event_.gap.read_rssi_complete.remote_addr, p->read_rssi_cmpl.remote_addr, sizeof(esp_bd_addr_t)); + break; + + // Security events - copy the entire security union + // Used by: ble_client, bluetooth_proxy, esp32_ble_client + case ESP_GAP_BLE_AUTH_CMPL_EVT: // Used by: bluetooth_proxy, esp32_ble_client + case ESP_GAP_BLE_SEC_REQ_EVT: // Used by: esp32_ble_client + case ESP_GAP_BLE_PASSKEY_NOTIF_EVT: // Used by: ble_client automation + case ESP_GAP_BLE_PASSKEY_REQ_EVT: // Used by: ble_client automation + case ESP_GAP_BLE_NC_REQ_EVT: // Used by: ble_client automation + memcpy(&this->event_.gap.security, &p->ble_security, sizeof(esp_ble_sec_t)); + break; + + default: + // We only store data for GAP events that components currently use + // Unknown events still get queued and logged in ble.cpp:375 as + // "Unhandled GAP event type in loop" - this helps identify new events + // that components might need in the future + break; + } + } + + // Initialize GATTC event data + void init_gattc_data_(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) { + this->event_.gattc.gattc_event = e; + this->event_.gattc.gattc_if = i; + + if (p == nullptr) { + this->event_.gattc.gattc_param = nullptr; + this->event_.gattc.data = nullptr; + return; // Invalid event, but we can't log in header file + } + + // Heap-allocate param and data + // Heap allocation is used because GATTC/GATTS events are rare (<1% of events) + // while GAP events (99%) are stored inline to minimize memory usage + // IMPORTANT: This heap allocation provides clear ownership semantics: + // - The BLEEvent owns the allocated memory for its lifetime + // - The data remains valid from the BLE callback context until processed in the main loop + // - Without this copy, we'd have use-after-free bugs as ESP-IDF reuses the callback memory + this->event_.gattc.gattc_param = new esp_ble_gattc_cb_param_t(*p); + + // Copy data for events that need it + // The param struct contains pointers (e.g., notify.value) that point to temporary buffers. + // We must copy this data to ensure it remains valid when the event is processed later. + switch (e) { + case ESP_GATTC_NOTIFY_EVT: + this->event_.gattc.data = new std::vector(p->notify.value, p->notify.value + p->notify.value_len); + this->event_.gattc.gattc_param->notify.value = this->event_.gattc.data->data(); + break; + case ESP_GATTC_READ_CHAR_EVT: + case ESP_GATTC_READ_DESCR_EVT: + this->event_.gattc.data = new std::vector(p->read.value, p->read.value + p->read.value_len); + this->event_.gattc.gattc_param->read.value = this->event_.gattc.data->data(); + break; + default: + this->event_.gattc.data = nullptr; + break; + } + } + + // Initialize GATTS event data + void init_gatts_data_(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) { + this->event_.gatts.gatts_event = e; + this->event_.gatts.gatts_if = i; + + if (p == nullptr) { + this->event_.gatts.gatts_param = nullptr; + this->event_.gatts.data = nullptr; + return; // Invalid event, but we can't log in header file + } + + // Heap-allocate param and data + // Heap allocation is used because GATTC/GATTS events are rare (<1% of events) + // while GAP events (99%) are stored inline to minimize memory usage + // IMPORTANT: This heap allocation provides clear ownership semantics: + // - The BLEEvent owns the allocated memory for its lifetime + // - The data remains valid from the BLE callback context until processed in the main loop + // - Without this copy, we'd have use-after-free bugs as ESP-IDF reuses the callback memory + this->event_.gatts.gatts_param = new esp_ble_gatts_cb_param_t(*p); + + // Copy data for events that need it + // The param struct contains pointers (e.g., write.value) that point to temporary buffers. + // We must copy this data to ensure it remains valid when the event is processed later. + switch (e) { + case ESP_GATTS_WRITE_EVT: + this->event_.gatts.data = new std::vector(p->write.value, p->write.value + p->write.len); + this->event_.gatts.gatts_param->write.value = this->event_.gatts.data->data(); + break; + default: + this->event_.gatts.data = nullptr; + break; + } + } }; +// Verify the gap_event struct hasn't grown beyond expected size +// The gap member in the union should be 80 bytes (including the gap_event enum) +static_assert(sizeof(decltype(((BLEEvent *) nullptr)->event_.gap)) <= 80, "gap_event struct has grown beyond 80 bytes"); + +// Verify esp_ble_sec_t fits within our union +static_assert(sizeof(esp_ble_sec_t) <= 73, "esp_ble_sec_t is larger than BLEScanResult"); + +// BLEEvent total size: 84 bytes (80 byte union + 1 byte type + 3 bytes padding) + } // namespace esp32_ble } // namespace esphome diff --git a/esphome/components/esp32_ble/ble_scan_result.h b/esphome/components/esp32_ble/ble_scan_result.h new file mode 100644 index 0000000000..49b0d5523d --- /dev/null +++ b/esphome/components/esp32_ble/ble_scan_result.h @@ -0,0 +1,24 @@ +#pragma once + +#ifdef USE_ESP32 + +#include + +namespace esphome { +namespace esp32_ble { + +// Structure for BLE scan results - only fields we actually use +struct __attribute__((packed)) BLEScanResult { + esp_bd_addr_t bda; + uint8_t ble_addr_type; + int8_t rssi; + uint8_t ble_adv[ESP_BLE_ADV_DATA_LEN_MAX + ESP_BLE_SCAN_RSP_DATA_LEN_MAX]; + uint8_t adv_data_len; + uint8_t scan_rsp_len; + uint8_t search_evt; +}; // ~73 bytes vs ~400 bytes for full esp_ble_gap_cb_param_t + +} // 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 c98477e121..0000000000 --- a/esphome/components/esp32_ble/queue.h +++ /dev/null @@ -1,56 +0,0 @@ -#pragma once - -#ifdef USE_ESP32 - -#include -#include - -#include -#include - -/* - * BLE events come in from a separate Task (thread) in the ESP32 stack. Rather - * than trying to deal with various locking strategies, all incoming GAP and GATT - * events will simply be placed on a semaphore guarded queue. The next time the - * component runs loop(), these events are popped off the queue and handed at - * this safer time. - */ - -namespace esphome { -namespace esp32_ble { - -template class Queue { - public: - Queue() { m_ = xSemaphoreCreateMutex(); } - - void push(T *element) { - if (element == nullptr) - return; - // It is not called from main loop. Thus it won't block main thread. - xSemaphoreTake(m_, portMAX_DELAY); - q_.push(element); - xSemaphoreGive(m_); - } - - T *pop() { - T *element = nullptr; - - if (xSemaphoreTake(m_, 5L / portTICK_PERIOD_MS)) { - if (!q_.empty()) { - element = q_.front(); - q_.pop(); - } - xSemaphoreGive(m_); - } - return element; - } - - protected: - std::queue q_; - SemaphoreHandle_t m_; -}; - -} // 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 61eed1c029..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 @@ -268,6 +266,7 @@ async def to_code(config): parent = await cg.get_variable(config[esp32_ble.CONF_BLE_ID]) cg.add(parent.register_gap_event_handler(var)) + cg.add(parent.register_gap_scan_event_handler(var)) cg.add(parent.register_gattc_event_handler(var)) cg.add(parent.register_ble_status_event_handler(var)) cg.add(var.set_parent(parent)) @@ -322,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] @@ -334,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 6d60f1638c..d950ccb5f1 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -50,17 +50,15 @@ void ESP32BLETracker::setup() { ESP_LOGE(TAG, "BLE Tracker was marked failed by ESP32BLE"); return; } - ExternalRAMAllocator allocator( - ExternalRAMAllocator::ALLOW_FAILURE); - this->scan_result_buffer_ = allocator.allocate(ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE); + RAMAllocator allocator; + this->scan_ring_buffer_ = allocator.allocate(SCAN_RESULT_BUFFER_SIZE); - if (this->scan_result_buffer_ == nullptr) { - ESP_LOGE(TAG, "Could not allocate buffer for BLE Tracker!"); + if (this->scan_ring_buffer_ == nullptr) { + ESP_LOGE(TAG, "Could not allocate ring buffer for BLE Tracker!"); this->mark_failed(); } global_esp32_ble_tracker = this; - this->scan_result_lock_ = xSemaphoreCreateMutex(); #ifdef USE_OTA ota::get_global_ota_callback()->add_on_state_callback( @@ -120,27 +118,31 @@ void ESP32BLETracker::loop() { } bool promote_to_connecting = discovered && !searching && !connecting; - if (this->scanner_state_ == ScannerState::RUNNING && - this->scan_result_index_ && // if it looks like we have a scan result we will take the lock - xSemaphoreTake(this->scan_result_lock_, 0)) { - uint32_t index = this->scan_result_index_; - if (index >= ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE) { - ESP_LOGW(TAG, "Too many BLE events to process. Some devices may not show up."); - } + // Process scan results from lock-free SPSC ring buffer + // 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) + uint8_t read_idx = this->ring_read_index_.load(std::memory_order_relaxed); - if (this->raw_advertisements_) { - for (auto *listener : this->listeners_) { - listener->parse_devices(this->scan_result_buffer_, this->scan_result_index_); - } - for (auto *client : this->clients_) { - client->parse_devices(this->scan_result_buffer_, this->scan_result_index_); - } - } + // Load producer's index with acquire to see their latest writes + uint8_t write_idx = this->ring_write_index_.load(std::memory_order_acquire); - if (this->parse_advertisements_) { - for (size_t i = 0; i < index; i++) { + while (read_idx != write_idx) { + // Process one result at a time directly from ring buffer + BLEScanResult &scan_result = this->scan_ring_buffer_[read_idx]; + + if (this->raw_advertisements_) { + for (auto *listener : this->listeners_) { + listener->parse_devices(&scan_result, 1); + } + for (auto *client : this->clients_) { + client->parse_devices(&scan_result, 1); + } + } + + if (this->parse_advertisements_) { ESPBTDevice device; - device.parse_scan_rst(this->scan_result_buffer_[i]); + device.parse_scan_rst(scan_result); bool found = false; for (auto *listener : this->listeners_) { @@ -161,9 +163,19 @@ void ESP32BLETracker::loop() { this->print_bt_device_info(device); } } + + // Move to next entry in ring buffer + read_idx = (read_idx + 1) % SCAN_RESULT_BUFFER_SIZE; + + // Store with release to ensure reads complete before index update + this->ring_read_index_.store(read_idx, std::memory_order_release); + } + + // Log dropped results periodically + size_t dropped = this->scan_results_dropped_.exchange(0, std::memory_order_relaxed); + if (dropped > 0) { + ESP_LOGW(TAG, "Dropped %zu BLE scan results due to buffer overflow", dropped); } - this->scan_result_index_ = 0; - xSemaphoreGive(this->scan_result_lock_); } if (this->scanner_state_ == ScannerState::STOPPED) { this->end_of_scan_(); // Change state to IDLE @@ -370,9 +382,6 @@ void ESP32BLETracker::recalculate_advertisement_parser_types() { void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { switch (event) { - case ESP_GAP_BLE_SCAN_RESULT_EVT: - this->gap_scan_result_(param->scan_rst); - break; case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: this->gap_scan_set_param_complete_(param->scan_param_cmpl); break; @@ -385,11 +394,57 @@ void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_ga default: break; } + // Forward all events to clients (scan results are handled separately via gap_scan_event_handler) for (auto *client : this->clients_) { client->gap_event_handler(event, param); } } +void ESP32BLETracker::gap_scan_event_handler(const BLEScanResult &scan_result) { + ESP_LOGV(TAG, "gap_scan_result - event %d", scan_result.search_evt); + + if (scan_result.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) { + // Lock-free SPSC ring buffer write (Producer side) + // This runs in the ESP-IDF Bluetooth stack callback thread + // IMPORTANT: Only this thread writes to ring_write_index_ + + // Load our own index with relaxed ordering (we're the only writer) + 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 + uint8_t read_idx = this->ring_read_index_.load(std::memory_order_acquire); + + // Check if buffer is full + if (next_write_idx != read_idx) { + // Write to ring buffer + this->scan_ring_buffer_[write_idx] = scan_result; + + // Store with release to ensure the write is visible before index update + this->ring_write_index_.store(next_write_idx, std::memory_order_release); + } else { + // Buffer full, track dropped results + this->scan_results_dropped_.fetch_add(1, std::memory_order_relaxed); + } + } else if (scan_result.search_evt == ESP_GAP_SEARCH_INQ_CMPL_EVT) { + // Scan finished on its own + if (this->scanner_state_ != ScannerState::RUNNING) { + if (this->scanner_state_ == ScannerState::STOPPING) { + ESP_LOGE(TAG, "Scan was not running when scan completed."); + } else if (this->scanner_state_ == ScannerState::STARTING) { + ESP_LOGE(TAG, "Scan was not started when scan completed."); + } else if (this->scanner_state_ == ScannerState::FAILED) { + ESP_LOGE(TAG, "Scan was in failed state when scan completed."); + } else if (this->scanner_state_ == ScannerState::IDLE) { + ESP_LOGE(TAG, "Scan was idle when scan completed."); + } else if (this->scanner_state_ == ScannerState::STOPPED) { + ESP_LOGE(TAG, "Scan was stopped when scan completed."); + } + } + this->set_scanner_state_(ScannerState::STOPPED); + } +} + void ESP32BLETracker::gap_scan_set_param_complete_(const esp_ble_gap_cb_param_t::ble_scan_param_cmpl_evt_param ¶m) { ESP_LOGV(TAG, "gap_scan_set_param_complete - status %d", param.status); if (param.status == ESP_BT_STATUS_DONE) { @@ -444,34 +499,6 @@ void ESP32BLETracker::gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_ this->set_scanner_state_(ScannerState::STOPPED); } -void ESP32BLETracker::gap_scan_result_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m) { - ESP_LOGV(TAG, "gap_scan_result - event %d", param.search_evt); - if (param.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) { - if (xSemaphoreTake(this->scan_result_lock_, 0)) { - if (this->scan_result_index_ < ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE) { - this->scan_result_buffer_[this->scan_result_index_++] = param; - } - xSemaphoreGive(this->scan_result_lock_); - } - } else if (param.search_evt == ESP_GAP_SEARCH_INQ_CMPL_EVT) { - // Scan finished on its own - if (this->scanner_state_ != ScannerState::RUNNING) { - if (this->scanner_state_ == ScannerState::STOPPING) { - ESP_LOGE(TAG, "Scan was not running when scan completed."); - } else if (this->scanner_state_ == ScannerState::STARTING) { - ESP_LOGE(TAG, "Scan was not started when scan completed."); - } else if (this->scanner_state_ == ScannerState::FAILED) { - ESP_LOGE(TAG, "Scan was in failed state when scan completed."); - } else if (this->scanner_state_ == ScannerState::IDLE) { - ESP_LOGE(TAG, "Scan was idle when scan completed."); - } else if (this->scanner_state_ == ScannerState::STOPPED) { - ESP_LOGE(TAG, "Scan was stopped when scan completed."); - } - } - this->set_scanner_state_(ScannerState::STOPPED); - } -} - void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { for (auto *client : this->clients_) { @@ -494,13 +521,16 @@ optional ESPBLEiBeacon::from_manufacturer_data(const ServiceData return ESPBLEiBeacon(data.data.data()); } -void ESPBTDevice::parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m) { - this->scan_result_ = param; +void ESPBTDevice::parse_scan_rst(const BLEScanResult &scan_result) { + this->scan_result_ = &scan_result; for (uint8_t i = 0; i < ESP_BD_ADDR_LEN; i++) - this->address_[i] = param.bda[i]; - this->address_type_ = param.ble_addr_type; - this->rssi_ = param.rssi; - this->parse_adv_(param); + this->address_[i] = scan_result.bda[i]; + this->address_type_ = static_cast(scan_result.ble_addr_type); + this->rssi_ = scan_result.rssi; + + // Parse advertisement data directly + uint8_t total_len = scan_result.adv_data_len + scan_result.scan_rsp_len; + this->parse_adv_(scan_result.ble_adv, total_len); #ifdef ESPHOME_LOG_HAS_VERY_VERBOSE ESP_LOGVV(TAG, "Parse Result:"); @@ -558,13 +588,13 @@ void ESPBTDevice::parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_e ESP_LOGVV(TAG, " Data: %s", format_hex_pretty(data.data).c_str()); } - ESP_LOGVV(TAG, " Adv data: %s", format_hex_pretty(param.ble_adv, param.adv_data_len + param.scan_rsp_len).c_str()); + ESP_LOGVV(TAG, " Adv data: %s", + format_hex_pretty(scan_result.ble_adv, scan_result.adv_data_len + scan_result.scan_rsp_len).c_str()); #endif } -void ESPBTDevice::parse_adv_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m) { + +void ESPBTDevice::parse_adv_(const uint8_t *payload, uint8_t len) { size_t offset = 0; - const uint8_t *payload = param.ble_adv; - uint8_t len = param.adv_data_len + param.scan_rsp_len; while (offset + 2 < len) { const uint8_t field_length = payload[offset++]; // First byte is length of adv record diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index eea73a7d26..f5ed75a93e 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -6,6 +6,7 @@ #include "esphome/core/helpers.h" #include +#include #include #include @@ -62,7 +63,7 @@ class ESPBLEiBeacon { class ESPBTDevice { public: - void parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m); + void parse_scan_rst(const BLEScanResult &scan_result); std::string address_str() const; @@ -84,7 +85,8 @@ class ESPBTDevice { const std::vector &get_service_datas() const { return service_datas_; } - const esp_ble_gap_cb_param_t::ble_scan_result_evt_param &get_scan_result() const { return scan_result_; } + // Exposed through a function for use in lambdas + const BLEScanResult &get_scan_result() const { return *scan_result_; } bool resolve_irk(const uint8_t *irk) const; @@ -98,7 +100,7 @@ class ESPBTDevice { } protected: - void parse_adv_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m); + void parse_adv_(const uint8_t *payload, uint8_t len); esp_bd_addr_t address_{ 0, @@ -112,7 +114,7 @@ class ESPBTDevice { std::vector service_uuids_{}; std::vector manufacturer_datas_{}; std::vector service_datas_{}; - esp_ble_gap_cb_param_t::ble_scan_result_evt_param scan_result_{}; + const BLEScanResult *scan_result_{nullptr}; }; class ESP32BLETracker; @@ -121,9 +123,7 @@ class ESPBTDeviceListener { public: virtual void on_scan_end() {} virtual bool parse_device(const ESPBTDevice &device) = 0; - virtual bool parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) { - return false; - }; + virtual bool parse_devices(const BLEScanResult *scan_results, size_t count) { return false; }; virtual AdvertisementParserType get_advertisement_parser_type() { return AdvertisementParserType::PARSED_ADVERTISEMENTS; }; @@ -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,19 +197,24 @@ 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, public GAPEventHandler, + public GAPScanEventHandler, public GATTcEventHandler, public BLEStatusEventHandler, public Parented { @@ -240,6 +245,7 @@ class ESP32BLETracker : public Component, 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 gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; + void gap_scan_event_handler(const BLEScanResult &scan_result) override; void ble_before_disabled_event_handler() override; void add_scanner_state_callback(std::function &&callback) { @@ -264,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_; @@ -285,14 +291,16 @@ class ESP32BLETracker : public Component, bool ble_was_disabled_{true}; bool raw_advertisements_{false}; bool parse_advertisements_{false}; - SemaphoreHandle_t scan_result_lock_; - size_t scan_result_index_{0}; -#ifdef USE_PSRAM - const static u_int8_t SCAN_RESULT_BUFFER_SIZE = 32; -#else - const static u_int8_t SCAN_RESULT_BUFFER_SIZE = 20; -#endif // USE_PSRAM - esp_ble_gap_cb_param_t::ble_scan_result_evt_param *scan_result_buffer_; + + // Lock-free Single-Producer Single-Consumer (SPSC) ring buffer for scan results + // Producer: ESP-IDF Bluetooth stack callback (gap_scan_event_handler) + // 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 + esp_bt_status_t scan_start_failed_{ESP_BT_STATUS_SUCCESS}; esp_bt_status_t scan_set_param_failed_{ESP_BT_STATUS_SUCCESS}; int connecting_{0}; 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 a7551571dd..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 @@ -57,7 +61,7 @@ void ESP32Camera::dump_config() { " External Clock: Pin:%d Frequency:%u\n" " I2C Pins: SDA:%d SCL:%d\n" " Reset Pin: %d", - this->name_.c_str(), YESNO(this->internal_), conf.pin_d0, conf.pin_d1, conf.pin_d2, conf.pin_d3, + this->name_.c_str(), YESNO(this->is_internal()), conf.pin_d0, conf.pin_d1, conf.pin_d2, conf.pin_d3, conf.pin_d4, conf.pin_d5, conf.pin_d6, conf.pin_d7, conf.pin_vsync, conf.pin_href, conf.pin_pclk, conf.pin_xclk, conf.xclk_freq_hz, conf.pin_sccb_sda, conf.pin_sccb_scl, conf.pin_reset); switch (this->config_.frame_size) { @@ -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 3899613854..0000000000 --- a/esphome/components/esp32_hall/esp32_hall.cpp +++ /dev/null @@ -1,24 +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); -} -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 019669406b..0000000000 --- a/esphome/components/esp32_hall/esp32_hall.h +++ /dev/null @@ -1,21 +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; -}; - -} // 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/fan/fan.cpp b/esphome/components/fan/fan.cpp index 87bf4939a0..25f710f893 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -41,39 +41,48 @@ void FanCall::perform() { void FanCall::validate_() { auto traits = this->parent_.get_traits(); - if (this->speed_.has_value()) + if (this->speed_.has_value()) { this->speed_ = clamp(*this->speed_, 1, traits.supported_speed_count()); - if (this->binary_state_.has_value() && *this->binary_state_) { - // when turning on, if neither current nor new speed available, set speed to 100% - if (traits.supports_speed() && !this->parent_.state && this->parent_.speed == 0 && !this->speed_.has_value()) { - this->speed_ = traits.supported_speed_count(); - } - } - - if (this->oscillating_.has_value() && !traits.supports_oscillation()) { - ESP_LOGW(TAG, "'%s' - This fan does not support oscillation!", this->parent_.get_name().c_str()); - this->oscillating_.reset(); - } - - if (this->speed_.has_value() && !traits.supports_speed()) { - ESP_LOGW(TAG, "'%s' - This fan does not support speeds!", this->parent_.get_name().c_str()); - this->speed_.reset(); - } - - if (this->direction_.has_value() && !traits.supports_direction()) { - ESP_LOGW(TAG, "'%s' - This fan does not support directions!", this->parent_.get_name().c_str()); - this->direction_.reset(); + // https://developers.home-assistant.io/docs/core/entity/fan/#preset-modes + // "Manually setting a speed must disable any set preset mode" + this->preset_mode_.clear(); } if (!this->preset_mode_.empty()) { const auto &preset_modes = traits.supported_preset_modes(); if (preset_modes.find(this->preset_mode_) == preset_modes.end()) { - ESP_LOGW(TAG, "'%s' - This fan does not support preset mode '%s'!", this->parent_.get_name().c_str(), - this->preset_mode_.c_str()); + ESP_LOGW(TAG, "%s: Preset mode '%s' not supported", this->parent_.get_name().c_str(), this->preset_mode_.c_str()); this->preset_mode_.clear(); } } + + // when turning on... + if (!this->parent_.state && this->binary_state_.has_value() && + *this->binary_state_ + // ..,and no preset mode will be active... + && this->preset_mode_.empty() && + this->parent_.preset_mode.empty() + // ...and neither current nor new speed is available... + && traits.supports_speed() && this->parent_.speed == 0 && !this->speed_.has_value()) { + // ...set speed to 100% + this->speed_ = traits.supported_speed_count(); + } + + if (this->oscillating_.has_value() && !traits.supports_oscillation()) { + ESP_LOGW(TAG, "%s: Oscillation not supported", this->parent_.get_name().c_str()); + this->oscillating_.reset(); + } + + if (this->speed_.has_value() && !traits.supports_speed()) { + ESP_LOGW(TAG, "%s: Speed control not supported", this->parent_.get_name().c_str()); + this->speed_.reset(); + } + + if (this->direction_.has_value() && !traits.supports_direction()) { + ESP_LOGW(TAG, "%s: Direction control not supported", this->parent_.get_name().c_str()); + this->direction_.reset(); + } } FanCall FanRestoreState::to_call(Fan &fan) { 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/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..18373edb77 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -175,7 +175,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) 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.cpp b/esphome/components/http_request/http_request_arduino.cpp index b4378cdce6..c009b33c2d 100644 --- a/esphome/components/http_request/http_request_arduino.cpp +++ b/esphome/components/http_request/http_request_arduino.cpp @@ -133,7 +133,6 @@ std::shared_ptr HttpRequestArduino::perform(std::string url, std: std::string header_value = container->client_.header(i).c_str(); ESP_LOGD(TAG, "Received response header, name: %s, value: %s", header_name.c_str(), header_value.c_str()); container->response_headers_[header_name].push_back(header_value); - break; } } 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/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp index 6a779ba03a..68c06d28f2 100644 --- a/esphome/components/http_request/http_request_idf.cpp +++ b/esphome/components/http_request/http_request_idf.cpp @@ -42,7 +42,6 @@ esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) { const std::string header_value = evt->header_value; ESP_LOGD(TAG, "Received response header, name: %s, value: %s", header_name.c_str(), header_value.c_str()); user_data->response_headers[header_name].push_back(header_value); - break; } break; } 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/i2c/__init__.py b/esphome/components/i2c/__init__.py index e47dec650d..6adb9b71aa 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -1,17 +1,20 @@ +import logging + from esphome import pins import esphome.codegen as cg +from esphome.components import esp32 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, CONF_TIMEOUT, + KEY_CORE, + KEY_FRAMEWORK_VERSION, PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040, @@ -19,11 +22,13 @@ from esphome.const import ( from esphome.core import CORE, coroutine_with_priority import esphome.final_validate as fv +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") @@ -40,20 +45,41 @@ def _bus_declare_type(value): raise NotImplementedError -pin_with_input_and_output_support = pins.internal_gpio_pin_number( - {CONF_OUTPUT: True, CONF_INPUT: True} -) +def validate_config(config): + if ( + config[CONF_SCAN] + and CORE.is_esp32 + and CORE.using_esp_idf + and esp32.get_esp32_variant() + in [ + esp32.const.VARIANT_ESP32C5, + esp32.const.VARIANT_ESP32C6, + esp32.const.VARIANT_ESP32P4, + ] + ): + version: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] + if version.major == 5 and ( + (version.minor == 3 and version.patch <= 3) + or (version.minor == 4 and version.patch <= 1) + ): + LOGGER.warning( + "There is a bug in esp-idf version %s that breaks I2C scan, I2C scan " + "has been disabled, see https://github.com/esphome/issues/issues/7128", + str(version), + ) + config[CONF_SCAN] = False + return config 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 ), @@ -65,12 +91,14 @@ CONFIG_SCHEMA = cv.All( } ).extend(cv.COMPONENT_SCHEMA), cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040]), + validate_config, ) @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) 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 2de3f1d9f8..7f233516e6 100644 --- a/esphome/components/i2s_audio/i2s_audio.cpp +++ b/esphome/components/i2s_audio/i2s_audio.cpp @@ -9,16 +9,12 @@ 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) { - ESP_LOGE(TAG, "Too many I2S Audio components"); + 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/microphone/i2s_audio_microphone.cpp b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp index 1ce98d51d3..0477e0682d 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp @@ -45,7 +45,7 @@ void I2SAudioMicrophone::setup() { #if SOC_I2S_SUPPORTS_ADC if (this->adc_) { if (this->parent_->get_port() != I2S_NUM_0) { - ESP_LOGE(TAG, "Internal ADC only works on I2S0!"); + ESP_LOGE(TAG, "Internal ADC only works on I2S0"); this->mark_failed(); return; } @@ -55,7 +55,7 @@ void I2SAudioMicrophone::setup() { { if (this->pdm_) { if (this->parent_->get_port() != I2S_NUM_0) { - ESP_LOGE(TAG, "PDM only works on I2S0!"); + ESP_LOGE(TAG, "PDM only works on I2S0"); this->mark_failed(); return; } @@ -64,14 +64,14 @@ void I2SAudioMicrophone::setup() { this->active_listeners_semaphore_ = xSemaphoreCreateCounting(MAX_LISTENERS, MAX_LISTENERS); if (this->active_listeners_semaphore_ == nullptr) { - ESP_LOGE(TAG, "Failed to create semaphore"); + ESP_LOGE(TAG, "Creating semaphore failed"); this->mark_failed(); return; } this->event_group_ = xEventGroupCreate(); if (this->event_group_ == nullptr) { - ESP_LOGE(TAG, "Failed to create event group"); + ESP_LOGE(TAG, "Creating event group failed"); this->mark_failed(); return; } @@ -79,6 +79,15 @@ void I2SAudioMicrophone::setup() { this->configure_stream_settings_(); } +void I2SAudioMicrophone::dump_config() { + ESP_LOGCONFIG(TAG, + "Microphone:\n" + " Pin: %d\n" + " PDM: %s\n" + " DC offset correction: %s", + static_cast(this->din_pin_), YESNO(this->pdm_), YESNO(this->correct_dc_offset_)); +} + void I2SAudioMicrophone::configure_stream_settings_() { uint8_t channel_count = 1; #ifdef USE_I2S_LEGACY @@ -127,6 +136,7 @@ bool I2SAudioMicrophone::start_driver_() { if (!this->parent_->try_lock()) { return false; // Waiting for another i2s to return lock } + this->locked_driver_ = true; esp_err_t err; #ifdef USE_I2S_LEGACY @@ -151,7 +161,7 @@ bool I2SAudioMicrophone::start_driver_() { config.mode = (i2s_mode_t) (config.mode | I2S_MODE_ADC_BUILT_IN); err = i2s_driver_install(this->parent_->get_port(), &config, 0, nullptr); if (err != ESP_OK) { - ESP_LOGE(TAG, "Error installing I2S driver: %s", esp_err_to_name(err)); + ESP_LOGE(TAG, "Error installing driver: %s", esp_err_to_name(err)); return false; } @@ -174,7 +184,7 @@ bool I2SAudioMicrophone::start_driver_() { err = i2s_driver_install(this->parent_->get_port(), &config, 0, nullptr); if (err != ESP_OK) { - ESP_LOGE(TAG, "Error installing I2S driver: %s", esp_err_to_name(err)); + ESP_LOGE(TAG, "Error installing driver: %s", esp_err_to_name(err)); return false; } @@ -183,7 +193,7 @@ bool I2SAudioMicrophone::start_driver_() { err = i2s_set_pin(this->parent_->get_port(), &pin_config); if (err != ESP_OK) { - ESP_LOGE(TAG, "Error setting I2S pin: %s", esp_err_to_name(err)); + ESP_LOGE(TAG, "Error setting pin: %s", esp_err_to_name(err)); return false; } } @@ -198,7 +208,7 @@ bool I2SAudioMicrophone::start_driver_() { /* Allocate a new RX channel and get the handle of this channel */ err = i2s_new_channel(&chan_cfg, NULL, &this->rx_handle_); if (err != ESP_OK) { - ESP_LOGE(TAG, "Error creating new I2S channel: %s", esp_err_to_name(err)); + ESP_LOGE(TAG, "Error creating channel: %s", esp_err_to_name(err)); return false; } @@ -270,14 +280,14 @@ bool I2SAudioMicrophone::start_driver_() { err = i2s_channel_init_std_mode(this->rx_handle_, &std_cfg); } if (err != ESP_OK) { - ESP_LOGE(TAG, "Error initializing I2S channel: %s", esp_err_to_name(err)); + ESP_LOGE(TAG, "Error initializing channel: %s", esp_err_to_name(err)); return false; } /* Before reading data, start the RX channel first */ i2s_channel_enable(this->rx_handle_); if (err != ESP_OK) { - ESP_LOGE(TAG, "Error enabling I2S Microphone: %s", esp_err_to_name(err)); + ESP_LOGE(TAG, "Enabling failed: %s", esp_err_to_name(err)); return false; } #endif @@ -304,31 +314,37 @@ void I2SAudioMicrophone::stop_driver_() { if (this->adc_) { err = i2s_adc_disable(this->parent_->get_port()); if (err != ESP_OK) { - ESP_LOGW(TAG, "Error disabling ADC - it may not have started: %s", esp_err_to_name(err)); + ESP_LOGW(TAG, "Error disabling ADC: %s", esp_err_to_name(err)); } } #endif err = i2s_stop(this->parent_->get_port()); if (err != ESP_OK) { - ESP_LOGW(TAG, "Error stopping I2S microphone - it may not have started: %s", esp_err_to_name(err)); + ESP_LOGW(TAG, "Error stopping: %s", esp_err_to_name(err)); } err = i2s_driver_uninstall(this->parent_->get_port()); if (err != ESP_OK) { - ESP_LOGW(TAG, "Error uninstalling I2S driver - it may not have started: %s", esp_err_to_name(err)); + ESP_LOGW(TAG, "Error uninstalling driver: %s", esp_err_to_name(err)); } #else - /* Have to stop the channel before deleting it */ - err = i2s_channel_disable(this->rx_handle_); - if (err != ESP_OK) { - ESP_LOGW(TAG, "Error stopping I2S microphone - it may not have started: %s", esp_err_to_name(err)); - } - /* If the handle is not needed any more, delete it to release the channel resources */ - err = i2s_del_channel(this->rx_handle_); - if (err != ESP_OK) { - ESP_LOGW(TAG, "Error deleting I2S channel - it may not have started: %s", esp_err_to_name(err)); + if (this->rx_handle_ != nullptr) { + /* Have to stop the channel before deleting it */ + err = i2s_channel_disable(this->rx_handle_); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Error stopping: %s", esp_err_to_name(err)); + } + /* If the handle is not needed any more, delete it to release the channel resources */ + err = i2s_del_channel(this->rx_handle_); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Error deleting channel: %s", esp_err_to_name(err)); + } + this->rx_handle_ = nullptr; } #endif - this->parent_->unlock(); + if (this->locked_driver_) { + this->parent_->unlock(); + this->locked_driver_ = false; + } } void I2SAudioMicrophone::mic_task(void *params) { @@ -400,7 +416,7 @@ size_t I2SAudioMicrophone::read_(uint8_t *buf, size_t len, TickType_t ticks_to_w // Ignore ESP_ERR_TIMEOUT if ticks_to_wait = 0, as it will read the data on the next call if (!this->status_has_warning()) { // Avoid spamming the logs with the error message if its repeated - ESP_LOGW(TAG, "Error reading from I2S microphone: %s", esp_err_to_name(err)); + ESP_LOGW(TAG, "Read error: %s", esp_err_to_name(err)); } this->status_set_warning(); return 0; @@ -428,19 +444,19 @@ void I2SAudioMicrophone::loop() { uint32_t event_group_bits = xEventGroupGetBits(this->event_group_); if (event_group_bits & MicrophoneEventGroupBits::TASK_STARTING) { - ESP_LOGD(TAG, "Task started, attempting to allocate buffer"); + ESP_LOGV(TAG, "Task started, attempting to allocate buffer"); xEventGroupClearBits(this->event_group_, MicrophoneEventGroupBits::TASK_STARTING); } if (event_group_bits & MicrophoneEventGroupBits::TASK_RUNNING) { - ESP_LOGD(TAG, "Task is running and reading data"); + ESP_LOGV(TAG, "Task is running and reading data"); xEventGroupClearBits(this->event_group_, MicrophoneEventGroupBits::TASK_RUNNING); this->state_ = microphone::STATE_RUNNING; } if ((event_group_bits & MicrophoneEventGroupBits::TASK_STOPPED)) { - ESP_LOGD(TAG, "Task finished, freeing resources and uninstalling I2S driver"); + ESP_LOGV(TAG, "Task finished, freeing resources and uninstalling driver"); vTaskDelete(this->task_handle_); this->task_handle_ = nullptr; @@ -470,7 +486,8 @@ void I2SAudioMicrophone::loop() { } if (!this->start_driver_()) { - this->status_momentary_error("I2S driver failed to start, unloading it and attempting again in 1 second", 1000); + ESP_LOGE(TAG, "Driver failed to start; retrying in 1 second"); + this->status_momentary_error("driver_fail", 1000); this->stop_driver_(); // Stop/frees whatever possibly started break; } @@ -480,7 +497,8 @@ void I2SAudioMicrophone::loop() { &this->task_handle_); if (this->task_handle_ == nullptr) { - this->status_momentary_error("Task failed to start, attempting again in 1 second", 1000); + ESP_LOGE(TAG, "Task failed to start, retrying in 1 second"); + this->status_momentary_error("task_fail", 1000); this->stop_driver_(); // Stops the driver to return the lock; will be reloaded in next attempt } } diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h index c35f88f8ee..5f66f2e962 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h @@ -18,6 +18,7 @@ namespace i2s_audio { class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, public Component { public: void setup() override; + void dump_config() override; void start() override; void stop() override; @@ -80,6 +81,7 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub bool pdm_{false}; bool correct_dc_offset_; + bool locked_driver_{false}; int32_t dc_offset_{0}; }; diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index f4c761ecc0..1042a7ebee 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -110,29 +110,48 @@ void I2SAudioSpeaker::setup() { } } +void I2SAudioSpeaker::dump_config() { + ESP_LOGCONFIG(TAG, + "Speaker:\n" + " Pin: %d\n" + " Buffer duration: %" PRIu32, + static_cast(this->dout_pin_), this->buffer_duration_ms_); + if (this->timeout_.has_value()) { + ESP_LOGCONFIG(TAG, " Timeout: %" PRIu32 " ms", this->timeout_.value()); + } +#ifdef USE_I2S_LEGACY +#if SOC_I2S_SUPPORTS_DAC + ESP_LOGCONFIG(TAG, " Internal DAC mode: %d", static_cast(this->internal_dac_mode_)); +#endif + ESP_LOGCONFIG(TAG, " Communication format: %d", static_cast(this->i2s_comm_fmt_)); +#else + ESP_LOGCONFIG(TAG, " Communication format: %s", this->i2s_comm_fmt_.c_str()); +#endif +} + void I2SAudioSpeaker::loop() { uint32_t event_group_bits = xEventGroupGetBits(this->event_group_); if (event_group_bits & SpeakerEventGroupBits::STATE_STARTING) { - ESP_LOGD(TAG, "Starting Speaker"); + ESP_LOGD(TAG, "Starting"); this->state_ = speaker::STATE_STARTING; xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::STATE_STARTING); } if (event_group_bits & SpeakerEventGroupBits::STATE_RUNNING) { - ESP_LOGD(TAG, "Started Speaker"); + ESP_LOGD(TAG, "Started"); this->state_ = speaker::STATE_RUNNING; xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::STATE_RUNNING); this->status_clear_warning(); this->status_clear_error(); } if (event_group_bits & SpeakerEventGroupBits::STATE_STOPPING) { - ESP_LOGD(TAG, "Stopping Speaker"); + ESP_LOGD(TAG, "Stopping"); this->state_ = speaker::STATE_STOPPING; xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::STATE_STOPPING); } if (event_group_bits & SpeakerEventGroupBits::STATE_STOPPED) { if (!this->task_created_) { - ESP_LOGD(TAG, "Stopped Speaker"); + ESP_LOGD(TAG, "Stopped"); this->state_ = speaker::STATE_STOPPED; xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ALL_BITS); this->speaker_task_handle_ = nullptr; @@ -140,20 +159,19 @@ void I2SAudioSpeaker::loop() { } if (event_group_bits & SpeakerEventGroupBits::ERR_TASK_FAILED_TO_START) { - this->status_set_error("Failed to start speaker task"); + this->status_set_error("Failed to start task"); xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ERR_TASK_FAILED_TO_START); } if (event_group_bits & SpeakerEventGroupBits::ALL_ERR_ESP_BITS) { uint32_t error_bits = event_group_bits & SpeakerEventGroupBits::ALL_ERR_ESP_BITS; - ESP_LOGW(TAG, "Error writing to I2S: %s", esp_err_to_name(err_bit_to_esp_err(error_bits))); + ESP_LOGW(TAG, "Writing failed: %s", esp_err_to_name(err_bit_to_esp_err(error_bits))); this->status_set_warning(); } if (event_group_bits & SpeakerEventGroupBits::ERR_ESP_NOT_SUPPORTED) { - this->status_set_error("Failed to adjust I2S bus to match the incoming audio"); - ESP_LOGE(TAG, - "Incompatible audio format: sample rate = %" PRIu32 ", channels = %" PRIu8 ", bits per sample = %" PRIu8, + this->status_set_error("Failed to adjust bus to match incoming audio"); + ESP_LOGE(TAG, "Incompatible audio format: sample rate = %" PRIu32 ", channels = %u, bits per sample = %u", this->audio_stream_info_.get_sample_rate(), this->audio_stream_info_.get_channels(), this->audio_stream_info_.get_bits_per_sample()); } @@ -202,7 +220,7 @@ void I2SAudioSpeaker::set_mute_state(bool mute_state) { size_t I2SAudioSpeaker::play(const uint8_t *data, size_t length, TickType_t ticks_to_wait) { if (this->is_failed()) { - ESP_LOGE(TAG, "Cannot play audio, speaker failed to setup"); + ESP_LOGE(TAG, "Setup failed; cannot play audio"); return 0; } if (this->state_ != speaker::STATE_RUNNING && this->state_ != speaker::STATE_STARTING) { @@ -466,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); } @@ -680,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/i2s_audio/speaker/i2s_audio_speaker.h b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h index b5e4b94bc4..eb2a0ae756 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h @@ -24,6 +24,7 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp float get_setup_priority() const override { return esphome::setup_priority::PROCESSOR; } void setup() override; + void dump_config() override; void loop() override; void set_buffer_duration(uint32_t buffer_duration_ms) { this->buffer_duration_ms_ = buffer_duration_ms; } 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/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/kmeteriso/kmeteriso.cpp b/esphome/components/kmeteriso/kmeteriso.cpp index b3fbc31fe6..714df0b538 100644 --- a/esphome/components/kmeteriso/kmeteriso.cpp +++ b/esphome/components/kmeteriso/kmeteriso.cpp @@ -19,9 +19,8 @@ void KMeterISOComponent::setup() { // Mark as not failed before initializing. Some devices will turn off sensors to save on batteries // and when they come back on, the COMPONENT_STATE_FAILED bit must be unset on the component. - if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) { - this->component_state_ &= ~COMPONENT_STATE_MASK; - this->component_state_ |= COMPONENT_STATE_CONSTRUCTION; + if (this->is_failed()) { + this->reset_to_construction_state(); } auto err = this->bus_->writev(this->address_, nullptr, 0); 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/esp_color_correction.h b/esphome/components/light/esp_color_correction.h index 979a1acb07..39ce5700c6 100644 --- a/esphome/components/light/esp_color_correction.h +++ b/esphome/components/light/esp_color_correction.h @@ -69,8 +69,8 @@ class ESPColorCorrection { protected: uint8_t gamma_table_[256]; uint8_t gamma_reverse_table_[256]; - Color max_brightness_; uint8_t local_brightness_{255}; + Color max_brightness_; }; } // namespace light diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index c2600d05c2..78b0ac9feb 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -136,7 +136,7 @@ LightColorValues LightCall::validate_() { // 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, + ESP_LOGW(TAG, "'%s' does not support color mode %s", name, LOG_STR_ARG(color_mode_to_human(this->color_mode_.value()))); this->color_mode_.reset(); } @@ -152,20 +152,20 @@ LightColorValues LightCall::validate_() { // 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); + ESP_LOGW(TAG, "'%s': setting brightness not supported", name); this->brightness_.reset(); } // 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); + ESP_LOGW(TAG, "'%s': transitions not supported", name); this->transition_length_.reset(); } // 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); + ESP_LOGW(TAG, "'%s': color mode does not support setting RGB brightness", name); this->color_brightness_.reset(); } @@ -173,7 +173,7 @@ LightColorValues LightCall::validate_() { 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 (!(color_mode & ColorCapability::RGB)) { - ESP_LOGW(TAG, "'%s' - This color mode does not support setting RGB color!", name); + ESP_LOGW(TAG, "'%s': color mode does not support setting RGB color", name); this->red_.reset(); this->green_.reset(); this->blue_.reset(); @@ -183,14 +183,14 @@ LightColorValues LightCall::validate_() { // White value exists check if (this->white_.has_value() && *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); + ESP_LOGW(TAG, "'%s': color mode does not support setting white value", name); this->white_.reset(); } // Color temperature exists check if (this->color_temperature_.has_value() && !(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); + ESP_LOGW(TAG, "'%s': color mode does not support setting color temperature", name); this->color_temperature_.reset(); } @@ -198,7 +198,7 @@ LightColorValues LightCall::validate_() { if ((this->cold_white_.has_value() && *this->cold_white_ > 0.0f) || (this->warm_white_.has_value() && *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); + ESP_LOGW(TAG, "'%s': color mode does not support setting cold/warm white value", name); this->cold_white_.reset(); this->warm_white_.reset(); } @@ -208,7 +208,7 @@ LightColorValues LightCall::validate_() { if (name_##_.has_value()) { \ auto val = *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)); \ } \ @@ -270,7 +270,7 @@ LightColorValues LightCall::validate_() { // Flash length check if (this->has_flash_() && *this->flash_length_ == 0) { - ESP_LOGW(TAG, "'%s' - Flash length must be greater than zero!", name); + ESP_LOGW(TAG, "'%s': flash length must be greater than zero", name); this->flash_length_.reset(); } @@ -284,18 +284,18 @@ LightColorValues LightCall::validate_() { // validate effect index if (this->has_effect_() && *this->effect_ > this->parent_->effects_.size()) { - ESP_LOGW(TAG, "'%s' - Invalid effect index %" PRIu32 "!", name, *this->effect_); + ESP_LOGW(TAG, "'%s': invalid effect index %" PRIu32, name, *this->effect_); this->effect_.reset(); } if (this->has_effect_() && (this->has_transition_() || this->has_flash_())) { - ESP_LOGW(TAG, "'%s' - Effect cannot be used together with transition/flash!", name); + ESP_LOGW(TAG, "'%s': effect cannot be used with transition/flash", name); this->transition_length_.reset(); this->flash_length_.reset(); } if (this->has_flash_() && this->has_transition_()) { - ESP_LOGW(TAG, "'%s' - Flash cannot be used together with transition!", name); + ESP_LOGW(TAG, "'%s': flash cannot be used with transition", name); this->transition_length_.reset(); } @@ -311,7 +311,7 @@ LightColorValues LightCall::validate_() { } if (this->has_transition_() && !supports_transition) { - ESP_LOGW(TAG, "'%s' - Light does not support transitions!", name); + ESP_LOGW(TAG, "'%s': transitions not supported", name); this->transition_length_.reset(); } @@ -320,7 +320,7 @@ LightColorValues LightCall::validate_() { // 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())) { if (this->has_effect_()) { - ESP_LOGW(TAG, "'%s' - Cannot start an effect when turning off!", name); + ESP_LOGW(TAG, "'%s': cannot start effect when turning off", name); this->effect_.reset(); } else if (this->parent_->active_effect_index_ != 0 && explicit_turn_off_request) { // Auto turn off effect @@ -348,7 +348,7 @@ void LightCall::transform_parameters_() { !(*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()); @@ -388,8 +388,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 +398,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,8 +406,8 @@ 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_() { @@ -472,7 +472,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; } diff --git a/esphome/components/light/light_color_values.h b/esphome/components/light/light_color_values.h index ca32b9c571..d8eaa6ae24 100644 --- a/esphome/components/light/light_color_values.h +++ b/esphome/components/light/light_color_values.h @@ -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; } diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index acba986f24..f21fb8a06e 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -17,7 +17,7 @@ namespace light { class LightOutput; -enum LightRestoreMode { +enum LightRestoreMode : uint8_t { LIGHT_RESTORE_DEFAULT_OFF, LIGHT_RESTORE_DEFAULT_ON, LIGHT_ALWAYS_OFF, @@ -212,12 +212,23 @@ class LightState : public EntityBase, public Component { /// Store the output to allow effects to have more access. LightOutput *output_; - /// Value for storing the index of the currently active effect. 0 if no effect is active - uint32_t active_effect_index_{}; /// The currently active transformer for this light (transition/flash). std::unique_ptr transformer_{nullptr}; + /// List of effects for this light. + std::vector effects_; + /// 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. + uint32_t default_transition_length_{}; + /// Transition length to use for flash transitions. + uint32_t flash_transition_length_{}; + /// Gamma correction factor for the light. + float gamma_correct_{}; + /// 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; /// Object used to store the persisted values of the light. ESPPreferenceObject rtc_; @@ -236,21 +247,11 @@ class LightState : public EntityBase, public Component { */ CallbackManager target_state_reached_callback_{}; - /// Default transition length for all transitions in ms. - uint32_t default_transition_length_{}; - /// Transition length to use for flash transitions. - uint32_t flash_transition_length_{}; - /// Gamma correction factor for the light. - float gamma_correct_{}; - /// Restore mode of the light. - LightRestoreMode restore_mode_; /// Initial state of the light. optional initial_state_{}; - /// List of effects for this light. - std::vector effects_; - // for effects, true if a transformer (transition) is active. - bool is_transformer_active_ = false; + /// Restore mode of the light. + LightRestoreMode restore_mode_; }; } // namespace light 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..3d4907aa6e 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -16,7 +16,11 @@ 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, +) import esphome.config_validation as cv from esphome.const import ( CONF_ARGS, @@ -35,6 +39,7 @@ from esphome.const import ( PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, + PLATFORM_LN882X, PLATFORM_RP2040, PLATFORM_RTL87XX, ) @@ -100,6 +105,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 +190,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 +223,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 +232,7 @@ CONFIG_SCHEMA = cv.All( PLATFORM_ESP32, PLATFORM_RP2040, PLATFORM_BK72XX, + PLATFORM_LN882X, PLATFORM_RTL87XX, ] ), @@ -324,7 +334,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: diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 59a3398ce8..a2c2aa0320 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,7 @@ 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, +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,32 +121,19 @@ 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->call_log_callbacks_(level, tag, this->tx_buffer_ + msg_start); + this->log_callback_.call(level, tag, this->tx_buffer_ + msg_start); 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; return this->current_level_; } -void HOT Logger::call_log_callbacks_(int level, const char *tag, const char *msg) { -#ifdef USE_ESP32 - // Suppress network-logging if memory constrained - // In some configurations (eg BLE enabled) there may be some transient - // memory exhaustion, and trying to log when OOM can lead to a crash. Skipping - // here usually allows the stack to recover instead. - // See issue #1234 for analysis. - if (xPortGetFreeHeapSize() < 2048) - return; -#endif - this->log_callback_.call(level, tag, msg); -} - Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate), tx_buffer_size_(tx_buffer_size) { // add 1 to buffer size for null terminator this->tx_buffer_ = new char[this->tx_buffer_size_ + 1]; // NOLINT @@ -152,6 +144,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 @@ -189,7 +185,7 @@ 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->call_log_callbacks_(message->level, message->tag, this->tx_buffer_); + this->log_callback_.call(message->level, message->tag, this->tx_buffer_); // 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); @@ -202,19 +198,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; } @@ -243,7 +243,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 6030d9e8f2..38faf73d84 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,29 +140,30 @@ 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: - void call_log_callbacks_(int level, const char *tag, const char *msg); void write_msg_(const char *msg); // 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 @@ -181,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 @@ -191,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->call_log_callbacks_(level, tag, this->tx_buffer_); + this->log_callback_.call(level, tag, this->tx_buffer_); } // 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; @@ -212,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); @@ -223,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_) { @@ -298,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; @@ -321,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); @@ -335,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 @@ -344,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 const uint16_t RESET_COLOR_LEN = strlen(ESPHOME_LOG_RESET_COLOR); 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) { if (level <= this->level_) { this->trigger(level, tag, message); } @@ -363,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..41445fa3b4 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 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/lvgl/widgets/qrcode.py b/esphome/components/lvgl/widgets/qrcode.py index 742b538938..7d8d13d8c4 100644 --- a/esphome/components/lvgl/widgets/qrcode.py +++ b/esphome/components/lvgl/widgets/qrcode.py @@ -3,7 +3,7 @@ import esphome.config_validation as cv from esphome.const import CONF_SIZE, CONF_TEXT from esphome.cpp_generator import MockObjClass -from ..defines import CONF_MAIN, literal +from ..defines import CONF_MAIN from ..lv_validation import color, color_retmapper, lv_text from ..lvcode import LocalVariable, lv, lv_expr from ..schemas import TEXT_SCHEMA @@ -34,7 +34,7 @@ class QrCodeType(WidgetType): ) def get_uses(self): - return ("canvas", "img") + return ("canvas", "img", "label") def obj_creator(self, parent: MockObjClass, config: dict): dark_color = color_retmapper(config[CONF_DARK_COLOR]) @@ -45,10 +45,8 @@ class QrCodeType(WidgetType): async def to_code(self, w: Widget, config): if (value := config.get(CONF_TEXT)) is not None: value = await lv_text.process(value) - with LocalVariable( - "qr_text", cg.const_char_ptr, value, modifier="" - ) as str_obj: - lv.qrcode_update(w.obj, str_obj, literal(f"strlen({str_obj})")) + with LocalVariable("qr_text", cg.std_string, value, modifier="") as str_obj: + lv.qrcode_update(w.obj, str_obj.c_str(), str_obj.size()) qr_code_spec = QrCodeType() 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/mcp23xxx_base/mcp23xxx_base.cpp b/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp index 14a703fb9f..fc49f216ee 100644 --- a/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp +++ b/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp @@ -6,7 +6,11 @@ namespace mcp23xxx_base { float MCP23XXXBase::get_setup_priority() const { return setup_priority::IO; } -void MCP23XXXGPIOPin::setup() { pin_mode(flags_); } +void MCP23XXXGPIOPin::setup() { + pin_mode(flags_); + this->parent_->pin_interrupt_mode(this->pin_, this->interrupt_mode_); +} + void MCP23XXXGPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } bool MCP23XXXGPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } void MCP23XXXGPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } 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..ed230d43aa 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -8,8 +8,6 @@ from esphome.const import ( CONF_PROTOCOL, CONF_SERVICE, CONF_SERVICES, - KEY_CORE, - KEY_FRAMEWORK_VERSION, ) from esphome.core import CORE, coroutine_with_priority @@ -85,15 +83,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") 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..f0d5a95d43 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -68,6 +68,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 +299,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 +455,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( { 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..20e0b4a499 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -176,7 +176,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 +229,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 +700,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/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index 55e36f9ad6..4d07d078c0 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -148,7 +148,7 @@ bool MQTTComponent::send_discovery_() { if (node_friendly_name.empty()) { node_friendly_name = node_name; } - const std::string &node_area = App.get_area(); + std::string node_area = App.get_area(); JsonObject device_info = root.createNestedObject(MQTT_DEVICE); const auto mac = get_mac_address(); 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/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 ab1e20859c..b6d4cc3f23 100644 --- a/esphome/components/nextion/binary_sensor/nextion_binarysensor.cpp +++ b/esphome/components/nextion/binary_sensor/nextion_binarysensor.cpp @@ -56,7 +56,7 @@ void NextionBinarySensor::set_state(bool state, bool publish, bool send_to_nexti this->publish_state(state); } else { this->state = state; - this->has_state_ = true; + this->set_has_state(true); } this->update_component_settings(); 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 3de32bfde9..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,26 @@ 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; } #endif // USE_NEXTION_COMMAND_SPACING @@ -43,44 +41,35 @@ bool Nextion::send_command_(const std::string &command) { const uint8_t to_send[3] = {0xFF, 0xFF, 0xFF}; this->write_array(to_send, sizeof(to_send)); -#ifdef USE_NEXTION_COMMAND_SPACING - this->command_pacer_.mark_sent(); -#endif // USE_NEXTION_COMMAND_SPACING - return true; } 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; @@ -97,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()); @@ -121,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) { @@ -147,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()); @@ -222,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_) { @@ -240,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)) { @@ -251,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]; @@ -292,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()) { @@ -377,12 +410,6 @@ void Nextion::process_nextion_commands_() { size_t commands_processed = 0; #endif // USE_NEXTION_MAX_COMMANDS_PER_LOOP -#ifdef USE_NEXTION_COMMAND_SPACING - if (!this->command_pacer_.can_send()) { - return; // Will try again in next loop iteration - } -#endif - size_t to_process_length = 0; std::string to_process; @@ -404,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_(); @@ -418,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_) { @@ -430,6 +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"); #endif break; case 0x02: // invalid Component ID or name was used @@ -547,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; } @@ -650,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 @@ -805,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++) { @@ -945,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()) { @@ -989,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(); } /** @@ -1011,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"); @@ -1037,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]; @@ -1072,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]; @@ -1111,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(), @@ -1139,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(), @@ -1156,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 @@ -1167,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"); @@ -1176,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()); @@ -1196,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"); @@ -1208,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) @@ -1237,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_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 fcd665917c..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,28 +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)); - 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(); - } - - if (successful) { - ESP_LOGD(TAG, "Restart"); - delay(1500); // NOLINT - App.safe_reboot(); - } else { - ESP_LOGE(TAG, "TFT upload failed"); - } - 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 8f54fbd8ac..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,28 +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)); - 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(); - } - - if (successful) { - ESP_LOGD(TAG, "Restart"); - delay(1500); // NOLINT - App.safe_reboot(); - } else { - ESP_LOGE(TAG, "TFT upload failed"); - } - 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 9be49e3476..0ed9da95d4 100644 --- a/esphome/components/nextion/sensor/nextion_sensor.cpp +++ b/esphome/components/nextion/sensor/nextion_sensor.cpp @@ -88,7 +88,7 @@ void NextionSensor::set_state(float state, bool publish, bool send_to_nextion) { } else { this->raw_state = state; this->state = state; - this->has_state_ = true; + this->set_has_state(true); } } this->update_component_settings(); diff --git a/esphome/components/nextion/text_sensor/nextion_textsensor.cpp b/esphome/components/nextion/text_sensor/nextion_textsensor.cpp index a1d45f55e0..e08cbb02ca 100644 --- a/esphome/components/nextion/text_sensor/nextion_textsensor.cpp +++ b/esphome/components/nextion/text_sensor/nextion_textsensor.cpp @@ -37,7 +37,7 @@ void NextionTextSensor::set_state(const std::string &state, bool publish, bool s this->publish_state(state); } else { this->state = state; - this->has_state_ = true; + this->set_has_state(true); } this->update_component_settings(); 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/number/number.cpp b/esphome/components/number/number.cpp index fda4f43e34..b6a845b19b 100644 --- a/esphome/components/number/number.cpp +++ b/esphome/components/number/number.cpp @@ -7,7 +7,7 @@ namespace number { static const char *const TAG = "number"; void Number::publish_state(float state) { - this->has_state_ = true; + this->set_has_state(true); this->state = state; ESP_LOGD(TAG, "'%s': Sending state %f", this->get_name().c_str(), state); this->state_callback_.call(state); diff --git a/esphome/components/number/number.h b/esphome/components/number/number.h index d839d12ad1..49bcbb857c 100644 --- a/esphome/components/number/number.h +++ b/esphome/components/number/number.h @@ -48,9 +48,6 @@ class Number : public EntityBase { NumberTraits traits; - /// Return whether this number has gotten a full state yet. - bool has_state() const { return has_state_; } - protected: friend class NumberCall; @@ -63,7 +60,6 @@ class Number : public EntityBase { virtual void control(float value) = 0; CallbackManager state_callback_; - bool has_state_{false}; }; } // namespace number 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 5b1ea491e3..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,62 +48,62 @@ 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}" - ) - 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}" - ) - 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:X}" - ) - if (pskc := config.get(CONF_PSKC)) is not None: - add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_PSKC", f"{pskc:X}") + 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, - cv.Optional(CONF_MESH_LOCAL_PREFIX): cv.hex_int, + cv.Optional(CONF_MESH_LOCAL_PREFIX): cv.ipv6network, } ) @@ -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.cpp b/esphome/components/openthread/openthread.cpp index f40a56952a..24b3c23960 100644 --- a/esphome/components/openthread/openthread.cpp +++ b/esphome/components/openthread/openthread.cpp @@ -137,7 +137,7 @@ void OpenThreadSrpComponent::setup() { // Copy the mdns services to our local instance so that the c_str pointers remain valid for the lifetime of this // component this->mdns_services_ = this->mdns_->get_services(); - ESP_LOGW(TAG, "Setting up SRP services. count = %d\n", this->mdns_services_.size()); + ESP_LOGD(TAG, "Setting up SRP services. count = %d\n", this->mdns_services_.size()); for (const auto &service : this->mdns_services_) { otSrpClientBuffersServiceEntry *entry = otSrpClientBuffersAllocateService(instance); if (!entry) { @@ -185,11 +185,11 @@ void OpenThreadSrpComponent::setup() { if (error != OT_ERROR_NONE) { ESP_LOGW(TAG, "Failed to add service: %s", otThreadErrorToString(error)); } - ESP_LOGW(TAG, "Added service: %s", full_service.c_str()); + ESP_LOGD(TAG, "Added service: %s", full_service.c_str()); } otSrpClientEnableAutoStartMode(instance, srp_start_callback, nullptr); - ESP_LOGW(TAG, "Finished SRP setup"); + ESP_LOGD(TAG, "Finished SRP setup"); } void *OpenThreadSrpComponent::pool_alloc_(size_t size) { 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 45c8c47227..0000000000 --- a/esphome/components/openthread/tlv.py +++ /dev/null @@ -1,58 +0,0 @@ -# Sourced from https://gist.github.com/agners/0338576e0003318b63ec1ea75adc90f9 -import binascii - -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") - 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/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/__init__.py b/esphome/components/prometheus/__init__.py index b899fe7642..26a9e70f7c 100644 --- a/esphome/components/prometheus/__init__.py +++ b/esphome/components/prometheus/__init__.py @@ -31,7 +31,6 @@ CONFIG_SCHEMA = cv.Schema( } ), }, - cv.only_with_arduino, ).extend(cv.COMPONENT_SCHEMA) 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..5de7d8c9c4 100644 --- a/esphome/components/remote_receiver/__init__.py +++ b/esphome/components/remote_receiver/__init__.py @@ -4,15 +4,12 @@ from esphome.components import esp32, esp32_rmt, remote_base 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, @@ -97,55 +94,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 +141,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) 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..713cee0186 100644 --- a/esphome/components/remote_transmitter/__init__.py +++ b/esphome/components/remote_transmitter/__init__.py @@ -4,14 +4,12 @@ from esphome.components import esp32, esp32_rmt, remote_base 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, ) @@ -38,34 +36,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 +65,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) 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/select/select.cpp b/esphome/components/select/select.cpp index 806882ad94..37887da27c 100644 --- a/esphome/components/select/select.cpp +++ b/esphome/components/select/select.cpp @@ -10,7 +10,7 @@ void Select::publish_state(const std::string &state) { auto index = this->index_of(state); const auto *name = this->get_name().c_str(); if (index.has_value()) { - this->has_state_ = true; + this->set_has_state(true); this->state = state; ESP_LOGD(TAG, "'%s': Sending state %s (index %zu)", name, state.c_str(), index.value()); this->state_callback_.call(state, index.value()); diff --git a/esphome/components/select/select.h b/esphome/components/select/select.h index 8ca9a69d1c..3ab651b241 100644 --- a/esphome/components/select/select.h +++ b/esphome/components/select/select.h @@ -35,9 +35,6 @@ class Select : public EntityBase { void publish_state(const std::string &state); - /// Return whether this select component has gotten a full state yet. - bool has_state() const { return has_state_; } - /// Instantiate a SelectCall object to modify this select component's state. SelectCall make_call() { return SelectCall(this); } @@ -73,7 +70,6 @@ class Select : public EntityBase { virtual void control(const std::string &value) = 0; CallbackManager state_callback_; - bool has_state_{false}; }; } // namespace select 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/sensor.cpp b/esphome/components/sensor/sensor.cpp index ed0a090866..0a82677bc9 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -23,22 +23,30 @@ 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; } void Sensor::publish_state(float state) { this->raw_state = state; - this->raw_callback_.call(state); + if (this->raw_callback_) { + this->raw_callback_->call(state); + } ESP_LOGV(TAG, "'%s': Received new state %f", this->name_.c_str(), state); @@ -51,7 +59,10 @@ void Sensor::publish_state(float state) { void Sensor::add_on_state_callback(std::function &&callback) { this->callback_.add(std::move(callback)); } void Sensor::add_on_raw_state_callback(std::function &&callback) { - this->raw_callback_.add(std::move(callback)); + if (!this->raw_callback_) { + this->raw_callback_ = make_unique>(); + } + this->raw_callback_->add(std::move(callback)); } void Sensor::add_filter(Filter *filter) { @@ -87,13 +98,12 @@ float Sensor::get_state() const { return this->state; } float Sensor::get_raw_state() const { return this->raw_state; } void Sensor::internal_send_state_to_frontend(float state) { - this->has_state_ = true; + this->set_has_state(true); this->state = state; ESP_LOGD(TAG, "'%s': Sending state %.5f %s with %d decimals of accuracy", this->get_name().c_str(), state, this->get_unit_of_measurement().c_str(), this->get_accuracy_decimals()); this->callback_.call(state); } -bool Sensor::has_state() const { return this->has_state_; } } // namespace sensor } // namespace esphome diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index 78fffd4f0c..e76196c8d9 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -7,6 +7,7 @@ #include "esphome/components/sensor/filter.h" #include +#include namespace esphome { namespace sensor { @@ -76,9 +77,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); @@ -143,15 +144,22 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa void internal_send_state_to_frontend(float state); protected: - CallbackManager raw_callback_; ///< Storage for raw state callbacks. - CallbackManager callback_; ///< Storage for filtered state callbacks. + std::unique_ptr> raw_callback_; ///< Storage for raw state callbacks (lazy allocated). + CallbackManager callback_; ///< Storage for filtered state callbacks. 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 - bool has_state_{false}; + // 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..26031a8da5 100644 --- a/esphome/components/socket/__init__.py +++ b/esphome/components/socket/__init__.py @@ -16,6 +16,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( diff --git a/esphome/components/speaker/media_player/audio_pipeline.cpp b/esphome/components/speaker/media_player/audio_pipeline.cpp index ac122b6e0c..333a076bec 100644 --- a/esphome/components/speaker/media_player/audio_pipeline.cpp +++ b/esphome/components/speaker/media_player/audio_pipeline.cpp @@ -343,13 +343,12 @@ void AudioPipeline::read_task(void *params) { xEventGroupSetBits(this_pipeline->event_group_, EventGroupBits::READER_MESSAGE_FINISHED); // Wait until the pipeline notifies us the source of the media file - EventBits_t event_bits = - xEventGroupWaitBits(this_pipeline->event_group_, - EventGroupBits::READER_COMMAND_INIT_FILE | EventGroupBits::READER_COMMAND_INIT_HTTP | - EventGroupBits::PIPELINE_COMMAND_STOP, // Bit message to read - pdFALSE, // Clear the bit on exit - pdFALSE, // Wait for all the bits, - portMAX_DELAY); // Block indefinitely until bit is set + EventBits_t event_bits = xEventGroupWaitBits( + this_pipeline->event_group_, + EventGroupBits::READER_COMMAND_INIT_FILE | EventGroupBits::READER_COMMAND_INIT_HTTP, // Bit message to read + pdFALSE, // Clear the bit on exit + pdFALSE, // Wait for all the bits, + portMAX_DELAY); // Block indefinitely until bit is set if (!(event_bits & EventGroupBits::PIPELINE_COMMAND_STOP)) { xEventGroupClearBits(this_pipeline->event_group_, EventGroupBits::READER_MESSAGE_FINISHED | @@ -434,12 +433,12 @@ void AudioPipeline::decode_task(void *params) { xEventGroupSetBits(this_pipeline->event_group_, EventGroupBits::DECODER_MESSAGE_FINISHED); // Wait until the reader notifies us that the media type is available - EventBits_t event_bits = xEventGroupWaitBits(this_pipeline->event_group_, - EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE | - EventGroupBits::PIPELINE_COMMAND_STOP, // Bit message to read - pdFALSE, // Clear the bit on exit - pdFALSE, // Wait for all the bits, - portMAX_DELAY); // Block indefinitely until bit is set + EventBits_t event_bits = + xEventGroupWaitBits(this_pipeline->event_group_, + EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE, // Bit message to read + pdFALSE, // Clear the bit on exit + pdFALSE, // Wait for all the bits, + portMAX_DELAY); // Block indefinitely until bit is set xEventGroupClearBits(this_pipeline->event_group_, EventGroupBits::DECODER_MESSAGE_FINISHED | EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE); diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py index ffb5e11f79..55a4b9c8f6 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -79,6 +79,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 +379,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 +391,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): 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 f7fe523a33..a34e3c3c82 100644 --- a/esphome/components/spi/spi_arduino.cpp +++ b/esphome/components/spi/spi_arduino.cpp @@ -3,7 +3,6 @@ namespace esphome { namespace spi { - #ifdef USE_ARDUINO static const char *const TAG = "spi-esp-arduino"; @@ -38,17 +37,28 @@ class SPIDelegateHw : public SPIDelegate { void write16(uint16_t data) override { this->channel_->transfer16(data); } -#ifdef USE_RP2040 void write_array(const uint8_t *ptr, size_t length) override { - // avoid overwriting the supplied buffer - uint8_t *rxbuf = new uint8_t[length]; // NOLINT(cppcoreguidelines-owning-memory) - memcpy(rxbuf, ptr, length); - this->channel_->transfer((void *) rxbuf, length); - delete[] rxbuf; // NOLINT(cppcoreguidelines-owning-memory) - } + if (length == 1) { + this->channel_->transfer(*ptr); + return; + } +#ifdef USE_RP2040 + 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 + if (reinterpret_cast(ptr) & 0x3) { + ESP_LOGVV(TAG, "SPI write buffer not word aligned, copying to temporary buffer"); + auto txbuf = std::vector(length); + memcpy(txbuf.data(), ptr, length); + this->channel_->writeBytes(txbuf.data(), length); + } else { + this->channel_->writeBytes(ptr, length); + } #else - void write_array(const uint8_t *ptr, size_t length) override { this->channel_->writeBytes(ptr, length); } + this->channel_->writeBytes(ptr, length); #endif + } void read_array(uint8_t *ptr, size_t length) override { this->channel_->transfer(ptr, length); } @@ -76,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/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/status_led/light/status_led_light.cpp b/esphome/components/status_led/light/status_led_light.cpp index 6d38833ebd..dc4820f6da 100644 --- a/esphome/components/status_led/light/status_led_light.cpp +++ b/esphome/components/status_led/light/status_led_light.cpp @@ -9,10 +9,10 @@ namespace status_led { static const char *const TAG = "status_led"; void StatusLEDLightOutput::loop() { - uint32_t new_state = App.get_app_state() & STATUS_LED_MASK; + uint8_t new_state = App.get_app_state() & STATUS_LED_MASK; if (new_state != this->last_app_state_) { - ESP_LOGV(TAG, "New app state 0x%08" PRIX32, new_state); + ESP_LOGV(TAG, "New app state 0x%02X", new_state); } if ((new_state & STATUS_LED_ERROR) != 0u) { diff --git a/esphome/components/status_led/light/status_led_light.h b/esphome/components/status_led/light/status_led_light.h index e711a2e749..bfa144526a 100644 --- a/esphome/components/status_led/light/status_led_light.h +++ b/esphome/components/status_led/light/status_led_light.h @@ -36,7 +36,7 @@ class StatusLEDLightOutput : public light::LightOutput, public Component { GPIOPin *pin_{nullptr}; output::BinaryOutput *output_{nullptr}; light::LightState *lightstate_{}; - uint32_t last_app_state_{0xFFFF}; + uint8_t last_app_state_{0xFF}; void output_state_(bool state); }; 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/switch/switch.h b/esphome/components/switch/switch.h index e8018ed36f..b999296564 100644 --- a/esphome/components/switch/switch.h +++ b/esphome/components/switch/switch.h @@ -21,7 +21,7 @@ const int RESTORE_MODE_PERSISTENT_MASK = 0x02; const int RESTORE_MODE_INVERTED_MASK = 0x04; const int RESTORE_MODE_DISABLED_MASK = 0x08; -enum SwitchRestoreMode { +enum SwitchRestoreMode : uint8_t { SWITCH_ALWAYS_OFF = !RESTORE_MODE_ON_MASK, SWITCH_ALWAYS_ON = RESTORE_MODE_ON_MASK, SWITCH_RESTORE_DEFAULT_OFF = RESTORE_MODE_PERSISTENT_MASK, @@ -49,12 +49,12 @@ class Switch : public EntityBase, public EntityBase_DeviceClass { */ void publish_state(bool state); - /// The current reported state of the binary sensor. - bool state; - /// Indicates whether or not state is to be retrieved from flash and how SwitchRestoreMode restore_mode{SWITCH_RESTORE_DEFAULT_OFF}; + /// The current reported state of the binary sensor. + bool state; + /** Turn this switch on. This is called by the front-end. * * For implementing switches, please override write_state. @@ -123,10 +123,16 @@ class Switch : public EntityBase, public EntityBase_DeviceClass { */ virtual void write_state(bool state) = 0; - CallbackManager state_callback_{}; - bool inverted_{false}; - Deduplicator publish_dedup_; + // Pointer first (4 bytes) ESPPreferenceObject rtc_; + + // CallbackManager (12 bytes on 32-bit - contains vector) + CallbackManager state_callback_{}; + + // Small types grouped together + Deduplicator publish_dedup_; // 2 bytes (bool has_value_ + bool last_value_) + bool inverted_{false}; // 1 byte + // Total: 3 bytes, 1 byte padding }; #define LOG_SWITCH(prefix, type, obj) log_switch((TAG), (prefix), LOG_STR_LITERAL(type), (obj)) 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/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/alarm_control_panel/template_alarm_control_panel.cpp b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp index c550d60630..6f743a77ef 100644 --- a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp @@ -110,15 +110,7 @@ void TemplateAlarmControlPanel::loop() { delay = this->arming_night_time_; } if ((millis() - this->last_update_) > delay) { -#ifdef USE_BINARY_SENSOR - for (auto sensor_info : this->sensor_map_) { - // Check for sensors left on and set to bypass automatically and remove them from monitoring - if ((sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_AUTO) && (sensor_info.first->state)) { - ESP_LOGW(TAG, "%s is left on and will be automatically bypassed", sensor_info.first->get_name().c_str()); - this->bypassed_sensor_indicies_.push_back(sensor_info.second.store_index); - } - } -#endif + this->bypass_before_arming(); this->publish_state(this->desired_state_); } return; @@ -259,10 +251,23 @@ void TemplateAlarmControlPanel::arm_(optional code, alarm_control_p if (delay > 0) { this->publish_state(ACP_STATE_ARMING); } else { + this->bypass_before_arming(); this->publish_state(state); } } +void TemplateAlarmControlPanel::bypass_before_arming() { +#ifdef USE_BINARY_SENSOR + for (auto sensor_info : this->sensor_map_) { + // Check for sensors left on and set to bypass automatically and remove them from monitoring + if ((sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_AUTO) && (sensor_info.first->state)) { + ESP_LOGW(TAG, "'%s' is left on and will be automatically bypassed", sensor_info.first->get_name().c_str()); + this->bypassed_sensor_indicies_.push_back(sensor_info.second.store_index); + } + } +#endif +} + void TemplateAlarmControlPanel::control(const AlarmControlPanelCall &call) { if (call.get_state()) { if (call.get_state() == ACP_STATE_ARMED_AWAY) { diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h index 0e3a5c77cf..c3b28e8efa 100644 --- a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h @@ -60,6 +60,7 @@ class TemplateAlarmControlPanel : public alarm_control_panel::AlarmControlPanel, bool get_requires_code_to_arm() const override { return this->requires_code_to_arm_; } bool get_all_sensors_ready() { return this->sensors_ready_; }; void set_restore_mode(TemplateAlarmControlPanelRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } + void bypass_before_arming(); #ifdef USE_BINARY_SENSOR /** Add a binary_sensor to the alarm_panel. 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/text.cpp b/esphome/components/text/text.cpp index 8f0242e747..654893d4e4 100644 --- a/esphome/components/text/text.cpp +++ b/esphome/components/text/text.cpp @@ -7,7 +7,7 @@ namespace text { static const char *const TAG = "text"; void Text::publish_state(const std::string &state) { - this->has_state_ = true; + this->set_has_state(true); this->state = state; if (this->traits.get_mode() == TEXT_MODE_PASSWORD) { ESP_LOGD(TAG, "'%s': Sending state " LOG_SECRET("'%s'"), this->get_name().c_str(), state.c_str()); diff --git a/esphome/components/text/text.h b/esphome/components/text/text.h index f71dde69ba..3cc0cefc3e 100644 --- a/esphome/components/text/text.h +++ b/esphome/components/text/text.h @@ -28,9 +28,6 @@ class Text : public EntityBase { void publish_state(const std::string &state); - /// Return whether this text input has gotten a full state yet. - bool has_state() const { return has_state_; } - /// Instantiate a TextCall object to modify this text component's state. TextCall make_call() { return TextCall(this); } @@ -48,7 +45,6 @@ class Text : public EntityBase { virtual void control(const std::string &value) = 0; CallbackManager state_callback_; - bool has_state_{false}; }; } // namespace text 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/text_sensor/text_sensor.cpp b/esphome/components/text_sensor/text_sensor.cpp index 736145e233..d38a3cd055 100644 --- a/esphome/components/text_sensor/text_sensor.cpp +++ b/esphome/components/text_sensor/text_sensor.cpp @@ -8,7 +8,9 @@ static const char *const TAG = "text_sensor"; void TextSensor::publish_state(const std::string &state) { this->raw_state = state; - this->raw_callback_.call(state); + if (this->raw_callback_) { + this->raw_callback_->call(state); + } ESP_LOGV(TAG, "'%s': Received new state %s", this->name_.c_str(), state.c_str()); @@ -53,14 +55,17 @@ void TextSensor::add_on_state_callback(std::function callback this->callback_.add(std::move(callback)); } void TextSensor::add_on_raw_state_callback(std::function callback) { - this->raw_callback_.add(std::move(callback)); + if (!this->raw_callback_) { + this->raw_callback_ = make_unique>(); + } + this->raw_callback_->add(std::move(callback)); } std::string TextSensor::get_state() const { return this->state; } std::string TextSensor::get_raw_state() const { return this->raw_state; } void TextSensor::internal_send_state_to_frontend(const std::string &state) { this->state = state; - this->has_state_ = true; + this->set_has_state(true); ESP_LOGD(TAG, "'%s': Sending state '%s'", this->name_.c_str(), state.c_str()); this->callback_.call(state); } diff --git a/esphome/components/text_sensor/text_sensor.h b/esphome/components/text_sensor/text_sensor.h index d67c8c8170..b54f75155b 100644 --- a/esphome/components/text_sensor/text_sensor.h +++ b/esphome/components/text_sensor/text_sensor.h @@ -6,6 +6,7 @@ #include "esphome/components/text_sensor/filter.h" #include +#include namespace esphome { namespace text_sensor { @@ -30,6 +31,8 @@ namespace text_sensor { class TextSensor : public EntityBase, public EntityBase_DeviceClass { public: + TextSensor() = default; + /// Getter-syntax for .state. std::string get_state() const; /// Getter-syntax for .raw_state @@ -59,17 +62,14 @@ class TextSensor : public EntityBase, public EntityBase_DeviceClass { // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) - bool has_state(); - void internal_send_state_to_frontend(const std::string &state); protected: - CallbackManager raw_callback_; ///< Storage for raw state callbacks. - CallbackManager callback_; ///< Storage for filtered state callbacks. + std::unique_ptr> + raw_callback_; ///< Storage for raw state callbacks (lazy allocated). + CallbackManager callback_; ///< Storage for filtered state callbacks. Filter *filter_list_{nullptr}; ///< Store all active filters. - - bool has_state_{false}; }; } // namespace text_sensor 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/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 84bd48d530..63b2579c3f 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -42,17 +42,13 @@ uart_config_t IDFUARTComponent::get_config_() { break; } - uart_config_t uart_config; + uart_config_t uart_config{}; uart_config.baud_rate = this->baud_rate_; uart_config.data_bits = data_bits; 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.cpp b/esphome/components/update/update_entity.cpp index ed9a0480d8..ce97fb1b77 100644 --- a/esphome/components/update/update_entity.cpp +++ b/esphome/components/update/update_entity.cpp @@ -30,7 +30,7 @@ void UpdateEntity::publish_state() { ESP_LOGD(TAG, " Progress: %.0f%%", this->update_info_.progress); } - this->has_state_ = true; + this->set_has_state(true); this->state_callback_.call(); } diff --git a/esphome/components/update/update_entity.h b/esphome/components/update/update_entity.h index cc269e288f..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" @@ -28,8 +29,6 @@ enum UpdateState : uint8_t { class UpdateEntity : public EntityBase, public EntityBase_DeviceClass { public: - bool has_state() const { return this->has_state_; } - void publish_state(); void perform() { this->perform(false); } @@ -40,13 +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_; - bool has_state_{false}; CallbackManager state_callback_{}; + std::unique_ptr> update_available_trigger_{nullptr}; }; } // namespace update diff --git a/esphome/components/uptime/sensor/uptime_timestamp_sensor.cpp b/esphome/components/uptime/sensor/uptime_timestamp_sensor.cpp index fa8cb2bb61..69033be11c 100644 --- a/esphome/components/uptime/sensor/uptime_timestamp_sensor.cpp +++ b/esphome/components/uptime/sensor/uptime_timestamp_sensor.cpp @@ -13,7 +13,7 @@ static const char *const TAG = "uptime.sensor"; void UptimeTimestampSensor::setup() { this->time_->add_on_time_sync_callback([this]() { - if (this->has_state_) + if (this->has_state()) return; // No need to update the timestamp if it's already set auto now = this->time_->now(); 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/__init__.py b/esphome/components/voice_assistant/__init__.py index b9309ab422..59c7ec8383 100644 --- a/esphome/components/voice_assistant/__init__.py +++ b/esphome/components/voice_assistant/__init__.py @@ -17,10 +17,11 @@ from esphome.const import ( AUTO_LOAD = ["socket"] DEPENDENCIES = ["api", "microphone"] -CODEOWNERS = ["@jesserockz"] +CODEOWNERS = ["@jesserockz", "@kahrendt"] CONF_ON_END = "on_end" CONF_ON_INTENT_END = "on_intent_end" +CONF_ON_INTENT_PROGRESS = "on_intent_progress" CONF_ON_INTENT_START = "on_intent_start" CONF_ON_LISTENING = "on_listening" CONF_ON_START = "on_start" @@ -136,6 +137,9 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_ON_INTENT_START): automation.validate_automation( single=True ), + cv.Optional(CONF_ON_INTENT_PROGRESS): automation.validate_automation( + single=True + ), cv.Optional(CONF_ON_INTENT_END): automation.validate_automation( single=True ), @@ -282,6 +286,13 @@ async def to_code(config): config[CONF_ON_INTENT_START], ) + if CONF_ON_INTENT_PROGRESS in config: + await automation.build_automation( + var.get_intent_progress_trigger(), + [(cg.std_string, "x")], + config[CONF_ON_INTENT_PROGRESS], + ) + if CONF_ON_INTENT_END in config: await automation.build_automation( var.get_intent_end_trigger(), diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index a692a7556e..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; } @@ -555,7 +555,7 @@ void VoiceAssistant::request_stop() { break; case State::AWAITING_RESPONSE: this->signal_stop_(); - break; + // Fallthrough intended to stop a streaming TTS announcement that has potentially started case State::STREAMING_RESPONSE: #ifdef USE_MEDIA_PLAYER // Stop any ongoing media player announcement @@ -599,6 +599,14 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { switch (msg.event_type) { case api::enums::VOICE_ASSISTANT_RUN_START: ESP_LOGD(TAG, "Assist Pipeline running"); +#ifdef USE_MEDIA_PLAYER + this->started_streaming_tts_ = false; + for (auto arg : msg.data) { + if (arg.name == "url") { + this->tts_response_url_ = std::move(arg.value); + } + } +#endif this->defer([this]() { this->start_trigger_->trigger(); }); break; case api::enums::VOICE_ASSISTANT_WAKE_WORD_START: @@ -622,6 +630,8 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { if (text.empty()) { ESP_LOGW(TAG, "No text in STT_END event"); return; + } else if (text.length() > 500) { + text = text.substr(0, 497) + "..."; } ESP_LOGD(TAG, "Speech recognised as: \"%s\"", text.c_str()); this->defer([this, text]() { this->stt_end_trigger_->trigger(text); }); @@ -631,6 +641,27 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { ESP_LOGD(TAG, "Intent started"); this->defer([this]() { this->intent_start_trigger_->trigger(); }); break; + case api::enums::VOICE_ASSISTANT_INTENT_PROGRESS: { + ESP_LOGD(TAG, "Intent progress"); + std::string tts_url_for_trigger = ""; +#ifdef USE_MEDIA_PLAYER + if (this->media_player_ != nullptr) { + for (const auto &arg : msg.data) { + if ((arg.name == "tts_start_streaming") && (arg.value == "1") && !this->tts_response_url_.empty()) { + this->media_player_->make_call().set_media_url(this->tts_response_url_).set_announcement(true).perform(); + + this->media_player_wait_for_announcement_start_ = true; + this->media_player_wait_for_announcement_end_ = false; + this->started_streaming_tts_ = true; + tts_url_for_trigger = this->tts_response_url_; + this->tts_response_url_.clear(); // Reset streaming URL + } + } + } +#endif + this->defer([this, tts_url_for_trigger]() { this->intent_progress_trigger_->trigger(tts_url_for_trigger); }); + break; + } case api::enums::VOICE_ASSISTANT_INTENT_END: { for (auto arg : msg.data) { if (arg.name == "conversation_id") { @@ -653,6 +684,9 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { ESP_LOGW(TAG, "No text in TTS_START event"); return; } + if (text.length() > 500) { + text = text.substr(0, 497) + "..."; + } ESP_LOGD(TAG, "Response: \"%s\"", text.c_str()); this->defer([this, text]() { this->tts_start_trigger_->trigger(text); @@ -678,7 +712,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { ESP_LOGD(TAG, "Response URL: \"%s\"", url.c_str()); this->defer([this, url]() { #ifdef USE_MEDIA_PLAYER - if (this->media_player_ != nullptr) { + if ((this->media_player_ != nullptr) && (!this->started_streaming_tts_)) { this->media_player_->make_call().set_media_url(url).set_announcement(true).perform(); this->media_player_wait_for_announcement_start_ = true; diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index 865731522f..2424ea6052 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -177,6 +177,7 @@ class VoiceAssistant : public Component { Trigger<> *get_intent_end_trigger() const { return this->intent_end_trigger_; } Trigger<> *get_intent_start_trigger() const { return this->intent_start_trigger_; } + Trigger *get_intent_progress_trigger() const { return this->intent_progress_trigger_; } Trigger<> *get_listening_trigger() const { return this->listening_trigger_; } Trigger<> *get_end_trigger() const { return this->end_trigger_; } Trigger<> *get_start_trigger() const { return this->start_trigger_; } @@ -233,6 +234,7 @@ class VoiceAssistant : public Component { Trigger<> *tts_stream_start_trigger_ = new Trigger<>(); Trigger<> *tts_stream_end_trigger_ = new Trigger<>(); #endif + Trigger *intent_progress_trigger_ = new Trigger(); Trigger<> *wake_word_detected_trigger_ = new Trigger<>(); Trigger *stt_end_trigger_ = new Trigger(); Trigger *tts_end_trigger_ = new Trigger(); @@ -268,6 +270,8 @@ class VoiceAssistant : public Component { #endif #ifdef USE_MEDIA_PLAYER media_player::MediaPlayer *media_player_{nullptr}; + std::string tts_response_url_{""}; + bool started_streaming_tts_{false}; bool media_player_wait_for_announcement_start_{false}; bool media_player_wait_for_announcement_end_{false}; #endif 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..575234e780 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_()); 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 870932d266..20ff1a7c29 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"; }); @@ -254,33 +298,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 +315,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 +350,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 +364,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 +394,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 +408,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 +437,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 +453,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 +477,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 +491,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 +525,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 +534,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 +561,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 +599,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 +613,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 +653,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 +680,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 +694,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 +759,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 +769,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 +796,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 +810,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 +854,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 +877,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 +891,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 +912,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 +938,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 +962,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 +987,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 +1007,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 +1021,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 +1046,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 +1065,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 +1079,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 +1104,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 +1124,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 +1138,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 +1185,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 +1199,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 +1221,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 +1241,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 +1258,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 +1308,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 +1358,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 +1417,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 +1451,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 +1465,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 +1502,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 +1523,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 +1537,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 +1552,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 +1591,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 +1604,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; @@ -1734,12 +1617,15 @@ void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMa request->send(404); } +static std::string get_event_type(event::Event *event) { return event->last_event_type ? *event->last_event_type : ""; } + std::string WebServer::event_state_json_generator(WebServer *web_server, void *source) { - return web_server->event_json((event::Event *) (source), *(((event::Event *) (source))->last_event_type), - DETAIL_STATE); + auto *event = static_cast(source); + return web_server->event_json(event, get_event_type(event), DETAIL_STATE); } std::string WebServer::event_all_json_generator(WebServer *web_server, void *source) { - return web_server->event_json((event::Event *) (source), *(((event::Event *) (source))->last_event_type), DETAIL_ALL); + auto *event = static_cast(source); + return web_server->event_json(event, get_event_type(event), DETAIL_ALL); } std::string WebServer::event_json(event::Event *obj, const std::string &event_type, JsonDetail start_config) { return json::build_json([this, obj, event_type, start_config](JsonObject root) { @@ -1753,12 +1639,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); } }); } @@ -1772,26 +1653,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; } @@ -1826,18 +1703,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; @@ -1859,116 +1731,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 @@ -2008,114 +1878,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; @@ -2123,14 +1996,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; @@ -2138,15 +2011,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}; } @@ -2154,16 +2043,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/__init__.py b/esphome/components/web_server_idf/__init__.py
index 73c51f8cb5..506e1c5c13 100644
--- a/esphome/components/web_server_idf/__init__.py
+++ b/esphome/components/web_server_idf/__init__.py
@@ -8,8 +8,6 @@ CONFIG_SCHEMA = cv.All(
     cv.only_with_esp_idf,
 )
 
-AUTO_LOAD = ["web_server"]
-
 
 async def to_code(config):
     # Increase the maximum supported size of headers section in HTTP request packet to be processed by the server
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 6bfc49c675..d2447681f5 100644
--- a/esphome/components/web_server_idf/web_server_idf.cpp
+++ b/esphome/components/web_server_idf/web_server_idf.cpp
@@ -1,18 +1,29 @@
 #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"
-
-#include "web_server_idf.h"
+#endif  // USE_WEBSERVER
 
 namespace esphome {
 namespace web_server_idf {
@@ -26,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_);
@@ -70,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);
@@ -273,6 +307,7 @@ void AsyncResponseStream::printf(const char *fmt, ...) {
   this->print(str);
 }
 
+#ifdef USE_WEBSERVER
 AsyncEventSource::~AsyncEventSource() {
   for (auto *ses : this->sessions_) {
     delete ses;  // NOLINT(cppcoreguidelines-owning-memory)
@@ -289,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);
+    }
   }
 }
 
@@ -328,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;
@@ -345,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_);
 
@@ -357,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
@@ -398,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;
@@ -420,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;
   }
 
@@ -511,6 +569,98 @@ 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 13a3ef168d..e8e40ef9b0 100644
--- a/esphome/components/web_server_idf/web_server_idf.h
+++ b/esphome/components/web_server_idf/web_server_idf.h
@@ -1,8 +1,10 @@
 #pragma once
 #ifdef USE_ESP_IDF
 
+#include "esphome/core/defines.h"
 #include 
 
+#include 
 #include 
 #include 
 #include 
@@ -12,10 +14,12 @@
 #include 
 
 namespace esphome {
+#ifdef USE_WEBSERVER
 namespace web_server {
 class WebServer;
 class ListEntitiesIterator;
 };  // namespace web_server
+#endif
 namespace web_server_idf {
 
 #define F(string_literal) (string_literal)
@@ -132,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;
@@ -200,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_{};
 };
@@ -208,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)
@@ -217,9 +224,10 @@ 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
 class AsyncEventSource;
 class AsyncEventSourceResponse;
 
@@ -267,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_;
@@ -286,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)
@@ -307,20 +315,20 @@ class AsyncEventSource : public AsyncWebHandler {
   connect_handler_t on_connect_{};
   esphome::web_server::WebServer *web_server_;
 };
+#endif  // USE_WEBSERVER
 
 class DefaultHeaders {
   friend class AsyncWebServerRequest;
+#ifdef USE_WEBSERVER
   friend class AsyncEventSourceResponse;
+#endif
 
  public:
   // NOLINTNEXTLINE(readability-identifier-naming)
   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/weikai/weikai.cpp b/esphome/components/weikai/weikai.cpp
index 2211fc77d5..ebe987cc65 100644
--- a/esphome/components/weikai/weikai.cpp
+++ b/esphome/components/weikai/weikai.cpp
@@ -102,7 +102,7 @@ WeikaiRegister &WeikaiRegister::operator|=(uint8_t value) {
 // The WeikaiComponent methods
 ///////////////////////////////////////////////////////////////////////////////
 void WeikaiComponent::loop() {
-  if ((this->component_state_ & COMPONENT_STATE_MASK) != COMPONENT_STATE_LOOP)
+  if (!this->is_in_loop_state())
     return;
 
   // If there are some bytes in the receive FIFO we transfers them to the ring buffers
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..e8ae9b1b4e 100644
--- a/esphome/components/wifi/__init__.py
+++ b/esphome/components/wifi/__init__.py
@@ -309,6 +309,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,
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_validation.py b/esphome/config_validation.py
index 964f533215..09b132a458 100644
--- a/esphome/config_validation.py
+++ b/esphome/config_validation.py
@@ -1,9 +1,19 @@
 """Helpers for config validation using voluptuous."""
 
+from __future__ import annotations
+
 from contextlib import contextmanager
 from dataclasses import dataclass
 from datetime import datetime
-from ipaddress import AddressValueError, IPv4Address, ip_address
+from ipaddress import (
+    AddressValueError,
+    IPv4Address,
+    IPv4Network,
+    IPv6Address,
+    IPv6Network,
+    ip_address,
+    ip_network,
+)
 import logging
 import os
 import re
@@ -21,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,
@@ -347,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.
 
@@ -1176,6 +1194,14 @@ def ipv4address(value):
     return address
 
 
+def ipv6address(value):
+    try:
+        address = IPv6Address(value)
+    except AddressValueError as exc:
+        raise Invalid(f"{value} is not a valid IPv6 address") from exc
+    return address
+
+
 def ipv4address_multi_broadcast(value):
     address = ipv4address(value)
     if not (address.is_multicast or (address == IPv4Address("255.255.255.255"))):
@@ -1193,6 +1219,33 @@ def ipaddress(value):
     return address
 
 
+def ipv4network(value):
+    """Validate that the value is a valid IPv4 network."""
+    try:
+        network = IPv4Network(value, strict=False)
+    except ValueError as exc:
+        raise Invalid(f"{value} is not a valid IPv4 network") from exc
+    return network
+
+
+def ipv6network(value):
+    """Validate that the value is a valid IPv6 network."""
+    try:
+        network = IPv6Network(value, strict=False)
+    except ValueError as exc:
+        raise Invalid(f"{value} is not a valid IPv6 network") from exc
+    return network
+
+
+def ipnetwork(value):
+    """Validate that the value is a valid IP network."""
+    try:
+        network = ip_network(value, strict=False)
+    except ValueError as exc:
+        raise Invalid(f"{value} is not a valid IP network") from exc
+    return network
+
+
 def _valid_topic(value):
     """Validate that this is a valid topic name/filter."""
     if value is None:  # Used to disable publishing and subscribing
@@ -1853,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,
     }
 )
 
@@ -1921,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 c01f30c3ff..4aeb5179e6 100644
--- a/esphome/const.py
+++ b/esphome/const.py
@@ -12,6 +12,7 @@ PLATFORM_ESP32 = "esp32"
 PLATFORM_ESP8266 = "esp8266"
 PLATFORM_HOST = "host"
 PLATFORM_LIBRETINY_OLDSTYLE = "libretiny"
+PLATFORM_LN882X = "ln882x"
 PLATFORM_RP2040 = "rp2040"
 PLATFORM_RTL87XX = "rtl87xx"
 
@@ -56,6 +57,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 +92,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 +220,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 +533,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 +654,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 +844,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 +1101,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 +1137,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 75a7052c63..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"
@@ -66,7 +67,7 @@ void Application::setup() {
                      [](Component *a, Component *b) { return a->get_loop_priority() > b->get_loop_priority(); });
 
     do {
-      uint32_t new_app_state = STATUS_LED_WARNING;
+      uint8_t new_app_state = STATUS_LED_WARNING;
       this->scheduler.call();
       this->feed_wdt();
       for (uint32_t j = 0; j <= i; j++) {
@@ -83,11 +84,15 @@ 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_();
 }
 void Application::loop() {
-  uint32_t new_app_state = 0;
+  uint8_t new_app_state = 0;
 
   this->scheduler.call();
 
@@ -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,12 +137,16 @@ 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
   auto elapsed = last_op_end_time - this->last_loop_;
   if (elapsed >= this->loop_interval_ || HighFrequencyLoopRequester::is_high_frequency()) {
-    yield();
+    // Even if we overran the loop interval, we still need to select()
+    // to know if any sockets have data ready
+    this->yield_with_select_(0);
   } else {
     uint32_t delay_time = this->loop_interval_ - elapsed;
     uint32_t next_schedule = this->scheduler.next_schedule_in().value_or(delay_time);
@@ -126,7 +155,7 @@ void Application::loop() {
     next_schedule = std::max(next_schedule, delay_time / 2);
     delay_time = std::min(next_schedule, delay_time);
 
-    this->delay_with_select_(delay_time);
+    this->yield_with_select_(delay_time);
   }
   this->last_loop_ = last_op_end_time;
 
@@ -160,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();
 }
@@ -174,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();
   }
 }
 
@@ -215,7 +244,7 @@ void Application::teardown_components(uint32_t timeout_ms) {
 
     // Give some time for I/O operations if components are still pending
     if (!pending_components.empty()) {
-      this->delay_with_select_(1);
+      this->yield_with_select_(1);
     }
 
     // Update time for next iteration
@@ -233,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;
   }
 }
 
@@ -293,8 +448,6 @@ bool Application::is_socket_ready(int fd) const {
   // This function is thread-safe for reading the result of select()
   // However, it should only be called after select() has been executed in the main loop
   // The read_fds_ is only modified by select() in the main loop
-  if (HighFrequencyLoopRequester::is_high_frequency())
-    return true;  // fd sets via select are not updated in high frequency looping - so force true fallback behavior
   if (fd < 0 || fd >= FD_SETSIZE)
     return false;
 
@@ -302,7 +455,9 @@ bool Application::is_socket_ready(int fd) const {
 }
 #endif
 
-void Application::delay_with_select_(uint32_t delay_ms) {
+void Application::yield_with_select_(uint32_t delay_ms) {
+  // Delay while monitoring sockets. When delay_ms is 0, always yield() to ensure other tasks run
+  // since select() with 0 timeout only polls without yielding.
 #ifdef USE_SOCKET_SELECT_SUPPORT
   if (!this->socket_fds_.empty()) {
     // Update fd_set if socket list has changed
@@ -340,6 +495,10 @@ void Application::delay_with_select_(uint32_t delay_ms) {
       ESP_LOGW(TAG, "select() failed with errno %d", errno);
       delay(delay_ms);
     }
+    // When delay_ms is 0, we need to yield since select(0) doesn't yield
+    if (delay_ms == 0) {
+      yield();
+    }
   } else {
     // No sockets registered, use regular delay
     delay(delay_ms);
diff --git a/esphome/core/application.h b/esphome/core/application.h
index d95f45e757..6ee05309ca 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,8 +96,8 @@ 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 std::string &area,
-                 const char *comment, const char *compilation_time, bool name_add_mac_suffix) {
+  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;
     if (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().
-  const std::string &get_area() const { return 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; }
 
@@ -332,8 +366,14 @@ class Application {
    */
   void teardown_components(uint32_t timeout_ms);
 
-  uint32_t get_app_state() const { return this->app_state_; }
+  uint8_t get_app_state() const { return this->app_state_; }
 
+#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) {
@@ -572,14 +612,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 delay_with_select_(uint32_t delay_ms);
+  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 +726,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_;
-  std::string area_;
-  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};
-  uint32_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 1141e4067d..9ef30081aa 100644
--- a/esphome/core/component.cpp
+++ b/esphome/core/component.cpp
@@ -1,7 +1,10 @@
 #include "esphome/core/component.h"
 
 #include 
+#include 
+#include 
 #include 
+#include 
 #include "esphome/core/application.h"
 #include "esphome/core/hal.h"
 #include "esphome/core/helpers.h"
@@ -11,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)
+static std::unique_ptr>> &get_component_error_messages() {
+  static std::unique_ptr>> instance;
+  return instance;
+}
+
+// Setup priority overrides - freed after setup completes
+// Typically < 5 entries, lazy allocated
+static std::unique_ptr>> &get_setup_priority_overrides() {
+  static std::unique_ptr>> instance;
+  return instance;
+}
+
 namespace setup_priority {
 
 const float BUS = 1000.0f;
@@ -29,18 +56,21 @@ const float LATE = -100.0f;
 
 }  // namespace setup_priority
 
-const uint32_t COMPONENT_STATE_MASK = 0xFF;
-const uint32_t COMPONENT_STATE_CONSTRUCTION = 0x00;
-const uint32_t COMPONENT_STATE_SETUP = 0x01;
-const uint32_t COMPONENT_STATE_LOOP = 0x02;
-const uint32_t COMPONENT_STATE_FAILED = 0x03;
-const uint32_t STATUS_LED_MASK = 0xFF00;
-const uint32_t STATUS_LED_OK = 0x0000;
-const uint32_t STATUS_LED_WARNING = 0x0100;
-const uint32_t STATUS_LED_ERROR = 0x0200;
+// 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;
+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 = 0x08;  // Bit 3
+const uint8_t STATUS_LED_ERROR = 0x10;    // Bit 4
 
-const uint32_t WARN_IF_BLOCKING_OVER_MS = 50U;       ///< Initial blocking time allowed without warning
-const uint32_t WARN_IF_BLOCKING_INCREMENT_MS = 10U;  ///< How long the blocking time must be larger to warn again
+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
 
 uint32_t global_state = 0;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
 
@@ -56,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);
@@ -73,22 +111,40 @@ 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 (get_component_error_messages()) {
+      for (const auto &pair : *get_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);
   }
 }
 
-uint32_t Component::get_component_state() const { return this->component_state_; }
+uint8_t Component::get_component_state() const { return this->component_state_; }
 void Component::call() {
-  uint32_t state = this->component_state_ & COMPONENT_STATE_MASK;
+  uint8_t state = this->component_state_ & COMPONENT_STATE_MASK;
   switch (state) {
     case COMPONENT_STATE_CONSTRUCTION:
       // State Construction: Call setup and set state to setup
@@ -109,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;
   }
@@ -120,19 +179,68 @@ const char *Component::get_component_source() const {
 }
 bool Component::should_warn_of_blocking(uint32_t blocking_time) {
   if (blocking_time > this->warn_if_blocking_over_) {
-    this->warn_if_blocking_over_ = blocking_time + WARN_IF_BLOCKING_INCREMENT_MS;
+    // Prevent overflow when adding increment - if we're about to overflow, just max out
+    if (blocking_time + WARN_IF_BLOCKING_INCREMENT_MS < blocking_time ||
+        blocking_time + WARN_IF_BLOCKING_INCREMENT_MS > std::numeric_limits::max()) {
+      this->warn_if_blocking_over_ = std::numeric_limits::max();
+    } else {
+      this->warn_if_blocking_over_ = static_cast(blocking_time + WARN_IF_BLOCKING_INCREMENT_MS);
+    }
     return true;
   }
   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());
+    this->component_state_ &= ~COMPONENT_STATE_MASK;
+    this->component_state_ |= COMPONENT_STATE_CONSTRUCTION;
+    // Clear error status when resetting
+    this->status_clear_error();
+  }
+}
+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);
@@ -140,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));
 }
@@ -172,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 (!get_component_error_messages()) {
+      get_component_error_messages() = std::make_unique>>();
+    }
+    // Check if this component already has an error message
+    for (auto &pair : *get_component_error_messages()) {
+      if (pair.first == this) {
+        pair.second = message;
+        return;
+      }
+    }
+    // Add new error message
+    get_component_error_messages()->emplace_back(this, message);
+  }
 }
 void Component::status_clear_warning() {
   if ((this->component_state_ & STATUS_LED_WARNING) == 0)
@@ -197,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 (get_setup_priority_overrides()) {
+    // Linear search is fine for small n (typically < 5 overrides)
+    for (const auto &pair : *get_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 (!get_setup_priority_overrides()) {
+    get_setup_priority_overrides() = std::make_unique>>();
+    // Reserve some space to avoid reallocations (most configs have < 10 overrides)
+    get_setup_priority_overrides()->reserve(10);
+  }
+
+  // Check if this component already has an override
+  for (auto &pair : *get_setup_priority_overrides()) {
+    if (pair.first == this) {
+      pair.second = priority;
+      return;
+    }
+  }
+
+  // Add new override
+  get_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)
@@ -254,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;
@@ -263,4 +412,9 @@ uint32_t WarnIfComponentBlockingGuard::finish() {
 
 WarnIfComponentBlockingGuard::~WarnIfComponentBlockingGuard() {}
 
+void clear_setup_priority_overrides() {
+  // Free the setup priority map completely
+  get_setup_priority_overrides().reset();
+}
+
 }  // namespace esphome
diff --git a/esphome/core/component.h b/esphome/core/component.h
index ce9f0289d0..3734473a02 100644
--- a/esphome/core/component.h
+++ b/esphome/core/component.h
@@ -53,19 +53,20 @@ static const uint32_t SCHEDULER_DONT_RUN = 4294967295UL;
     ESP_LOGCONFIG(TAG, "  Update Interval: %.1fs", this->get_update_interval() / 1000.0f); \
   }
 
-extern const uint32_t COMPONENT_STATE_MASK;
-extern const uint32_t COMPONENT_STATE_CONSTRUCTION;
-extern const uint32_t COMPONENT_STATE_SETUP;
-extern const uint32_t COMPONENT_STATE_LOOP;
-extern const uint32_t COMPONENT_STATE_FAILED;
-extern const uint32_t STATUS_LED_MASK;
-extern const uint32_t STATUS_LED_OK;
-extern const uint32_t STATUS_LED_WARNING;
-extern const uint32_t STATUS_LED_ERROR;
+extern const uint8_t COMPONENT_STATE_MASK;
+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;
+extern const uint8_t STATUS_LED_ERROR;
 
 enum class RetryResult { DONE, RETRY };
 
-extern const uint32_t WARN_IF_BLOCKING_OVER_MS;
+extern const uint16_t WARN_IF_BLOCKING_OVER_MS;
 
 class Component {
  public:
@@ -123,7 +124,19 @@ class Component {
    */
   virtual void on_powerdown() {}
 
-  uint32_t get_component_state() const;
+  uint8_t get_component_state() const;
+
+  /** Reset this component back to the construction state to allow setup to run again.
+   *
+   * This can be used by components that have recoverable failures to attempt setup again.
+   */
+  void reset_to_construction_state();
+
+  /** Check if this component has completed setup and is in the loop state.
+   *
+   * @return True if in loop state, false otherwise.
+   */
+  bool is_in_loop_state() const;
 
   /** Mark this component as failed. Any future timeouts/intervals/setup/loop will no longer be called.
    *
@@ -138,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;
@@ -206,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.
@@ -214,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.
    *
@@ -274,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.
@@ -282,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.
    *
@@ -292,17 +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
 
-  uint32_t component_state_{0x0000};  ///< State of this component.
-  float setup_priority_override_{NAN};
+  // Ordered for optimal packing on 32-bit systems
   const char *component_source_{nullptr};
-  uint32_t warn_if_blocking_over_{WARN_IF_BLOCKING_OVER_MS};
-  std::string error_message_{};
+  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};
+  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.
@@ -364,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..641c73a292 100644
--- a/esphome/core/config.py
+++ b/esphome/core/config.py
@@ -1,18 +1,24 @@
+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
 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,
@@ -32,7 +38,13 @@ from esphome.const import (
     __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 +60,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 +84,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 +174,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 +249,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 +424,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 +435,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 +504,50 @@ 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))
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..4115b97391 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,6 +31,7 @@
 #define USE_DATETIME_DATETIME
 #define USE_DATETIME_TIME
 #define USE_DEEP_SLEEP
+#define USE_DEVICES
 #define USE_DISPLAY
 #define USE_ESP32_IMPROV_STATE_CALLBACK
 #define USE_EVENT
@@ -84,6 +87,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 +103,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 +118,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 +138,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 +155,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 +191,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 +208,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 725a8569a3..6afd02ff65 100644
--- a/esphome/core/entity_base.cpp
+++ b/esphome/core/entity_base.cpp
@@ -11,21 +11,20 @@ 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());
-    this->has_own_name_ = false;
+#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->has_own_name_ = true;
+    this->flags_.has_own_name = true;
   }
 }
 
-// Entity Internal
-bool EntityBase::is_internal() const { return this->internal_; }
-void EntityBase::set_internal(bool internal) { this->internal_ = internal; }
-
-// Entity Disabled by Default
-bool EntityBase::is_disabled_by_default() const { return this->disabled_by_default_; }
-void EntityBase::set_disabled_by_default(bool disabled_by_default) { this->disabled_by_default_ = disabled_by_default; }
-
 // Entity Icon
 std::string EntityBase::get_icon() const {
   if (this->icon_c_str_ == nullptr) {
@@ -35,14 +34,10 @@ std::string EntityBase::get_icon() const {
 }
 void EntityBase::set_icon(const char *icon) { this->icon_c_str_ = icon; }
 
-// Entity Category
-EntityCategory EntityBase::get_entity_category() const { return this->entity_category_; }
-void EntityBase::set_entity_category(EntityCategory entity_category) { this->entity_category_ = entity_category; }
-
 // Entity Object ID
 std::string EntityBase::get_object_id() const {
   // Check if `App.get_friendly_name()` is constant or dynamic.
-  if (!this->has_own_name_ && App.is_name_add_mac_suffix_enabled()) {
+  if (!this->flags_.has_own_name && App.is_name_add_mac_suffix_enabled()) {
     // `App.get_friendly_name()` is dynamic.
     return str_sanitize(str_snake_case(App.get_friendly_name()));
   } else {
@@ -59,19 +54,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->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 4ca21f9ee5..4819b66108 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 {
 
@@ -20,7 +26,7 @@ class EntityBase {
   void set_name(const char *name);
 
   // Get whether this Entity has its own name or it should use the device friendly_name.
-  bool has_own_name() const { return this->has_own_name_; }
+  bool has_own_name() const { return this->flags_.has_own_name; }
 
   // Get the sanitized name of this Entity as an ID.
   std::string get_object_id() const;
@@ -29,24 +35,43 @@ class EntityBase {
   // Get the unique Object ID of this Entity
   uint32_t get_object_id_hash();
 
-  // Get/set whether this Entity should be hidden from outside of ESPHome
-  bool is_internal() const;
-  void set_internal(bool internal);
+  // Get/set whether this Entity should be hidden outside ESPHome
+  bool is_internal() const { return this->flags_.internal; }
+  void set_internal(bool internal) { this->flags_.internal = internal; }
 
   // Check if this object is declared to be disabled by default.
   // That means that when the device gets added to Home Assistant (or other clients) it should
   // not be added to the default view by default, and a user action is necessary to manually add it.
-  bool is_disabled_by_default() const;
-  void set_disabled_by_default(bool disabled_by_default);
+  bool is_disabled_by_default() const { return this->flags_.disabled_by_default; }
+  void set_disabled_by_default(bool disabled_by_default) { this->flags_.disabled_by_default = disabled_by_default; }
 
   // Get/set the entity category.
-  EntityCategory get_entity_category() const;
-  void set_entity_category(EntityCategory entity_category);
+  EntityCategory get_entity_category() const { return static_cast(this->flags_.entity_category); }
+  void set_entity_category(EntityCategory entity_category) {
+    this->flags_.entity_category = static_cast(entity_category);
+  }
 
   // Get/set this entity's icon
   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; }
+
+  // Set has_state - for components that need to manually set this
+  void set_has_state(bool state) { this->flags_.has_state = state; }
+
  protected:
   /// The hash_base() function has been deprecated. It is kept in this
   /// class for now, to prevent external components from not compiling.
@@ -56,11 +81,20 @@ class EntityBase {
   StringRef name_;
   const char *object_id_c_str_{nullptr};
   const char *icon_c_str_{nullptr};
-  uint32_t object_id_hash_;
-  bool has_own_name_{false};
-  bool internal_{false};
-  bool disabled_by_default_{false};
-  EntityCategory entity_category_{ENTITY_CATEGORY_NONE};
+  uint32_t object_id_hash_{};
+#ifdef USE_DEVICES
+  Device *device_{};
+#endif
+
+  // Bit-packed flags to save memory (1 byte instead of 5)
+  struct EntityFlags {
+    uint8_t has_own_name : 1;
+    uint8_t internal : 1;
+    uint8_t disabled_by_default : 1;
+    uint8_t has_state : 1;
+    uint8_t entity_category : 2;  // Supports up to 4 categories
+    uint8_t reserved : 2;         // Reserved for future use
+  } flags_{};
 };
 
 class EntityBase_DeviceClass {  // NOLINT(readability-identifier-naming)
@@ -85,4 +119,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..2442fbca4b 100644
--- a/esphome/core/entity_helpers.py
+++ b/esphome/core/entity_helpers.py
@@ -1,5 +1,116 @@
-from esphome.const import CONF_ID
+from collections.abc import Callable
+import logging
+
+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(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 +165,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..72722169d4 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;
@@ -404,6 +293,21 @@ std::string format_hex_pretty(const uint16_t *data, size_t 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::string &data) {
+  if (data.empty())
+    return "";
+  std::string ret;
+  ret.resize(3 * data.length() - 1);
+  for (size_t i = 0; i < data.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 != data.length() - 1)
+      ret[3 * i + 2] = '.';
+  }
+  if (data.length() > 4)
+    return ret + " (" + std::to_string(data.length()) + ")";
+  return ret;
+}
 
 std::string format_bin(const uint8_t *data, size_t length) {
   std::string result;
@@ -640,35 +544,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 +559,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 +568,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 7d25e7d261..d92cf07702 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.
@@ -423,6 +352,8 @@ std::string format_hex_pretty(const uint16_t *data, size_t length);
 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 the string \p data in pretty-printed, human-readable hex.
+std::string format_hex_pretty(const std::string &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) {
   val = convert_big_endian(val);
@@ -438,7 +369,7 @@ template::value, int> = 0> std::stri
 }
 
 /// Return values for parse_on_off().
-enum ParseOnOffState {
+enum ParseOnOffState : uint8_t {
   PARSE_NONE = 0,
   PARSE_ON,
   PARSE_OFF,
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..515f6fd355 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,75 +18,149 @@ 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 =
+      is_static_string ? static_cast(name_ptr) : static_cast(name_ptr)->c_str();
 
-  if (!name.empty())
-    this->cancel_timeout(component, name);
+  // Cancel existing timer if name is not empty
+  if (name_cstr != nullptr && name_cstr[0] != '\0') {
+    this->cancel_item_(component, name_cstr, type);
+  }
 
-  if (timeout == SCHEDULER_DONT_RUN)
+  if (delay == SCHEDULER_DONT_RUN)
     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->defer_queue_.push_back(std::move(item));
+    return;
+  }
 #endif
+
+  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
+
   this->push_(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);
 }
+bool HOT Scheduler::cancel_timeout(Component *component, const char *name) {
+  return this->cancel_item_(component, 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);
 }
+bool HOT Scheduler::cancel_interval(Component *component, const char *name) {
+  return this->cancel_item_(component, name, SchedulerItem::INTERVAL);
+}
 
 struct RetryArgs {
   std::function func;
   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;
 };
@@ -145,6 +220,35 @@ 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).
+  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,7 +258,7 @@ 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();
@@ -162,8 +266,9 @@ void HOT Scheduler::call() {
       this->pop_raw_();
       this->lock_.unlock();
 
+      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));
@@ -217,24 +322,17 @@ 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());
     }
 
     {
@@ -298,34 +396,90 @@ void HOT Scheduler::push_(std::unique_ptr item) {
   LockGuard guard{this->lock_};
   this->to_add_.push_back(std::move(item));
 }
-bool HOT Scheduler::cancel_item_(Component *component, const std::string &name, Scheduler::SchedulerItem::Type type) {
+// Helper function to check if item matches criteria for cancellation
+bool HOT Scheduler::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();
+  return item_name != nullptr && strcmp(name_cstr, item_name) == 0;
+}
+
+// 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();
+}
+
+// Common implementation for cancel operations
+bool HOT Scheduler::cancel_item_common_(Component *component, bool is_static_string, const void *name_ptr,
+                                        SchedulerItem::Type type) {
+  // Get the name as const char*
+  const char *name_cstr =
+      is_static_string ? static_cast(name_ptr) : static_cast(name_ptr)->c_str();
+
+  // Handle null or empty names
+  if (name_cstr == nullptr)
+    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;
+
+  // Check all containers for matching items
+#if !defined(USE_ESP8266) && !defined(USE_RP2040)
+  // Only check defer_queue_ on platforms that have it
+  for (auto &item : this->defer_queue_) {
+    if (this->matches_item_(item, component, name_cstr, type)) {
+      item->remove = true;
       ret = true;
     }
   }
-  for (auto &it : this->to_add_) {
-    if (it->component == component && it->name == name && it->type == type) {
-      it->remove = true;
+#endif
+
+  for (auto &item : this->items_) {
+    if (this->matches_item_(item, component, name_cstr, type)) {
+      item->remove = true;
+      ret = true;
+      this->to_remove_++;  // Only track removals for heap items
+    }
+  }
+
+  for (auto &item : this->to_add_) {
+    if (this->matches_item_(item, component, name_cstr, type)) {
+      item->remove = true;
       ret = true;
     }
   }
 
   return ret;
 }
+
+bool HOT Scheduler::cancel_item_(Component *component, const std::string &name, Scheduler::SchedulerItem::Type type) {
+  return this->cancel_item_common_(component, false, &name, type);
+}
+
+bool HOT Scheduler::cancel_item_(Component *component, const char *name, SchedulerItem::Type type) {
+  return this->cancel_item_common_(component, true, name, type);
+}
+
 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..bf5e63cccf 100644
--- a/esphome/core/scheduler.h
+++ b/esphome/core/scheduler.h
@@ -2,6 +2,7 @@
 
 #include 
 #include 
+#include 
 
 #include "esphome/core/component.h"
 #include "esphome/core/helpers.h"
@@ -12,11 +13,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 +59,106 @@ 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;
+
+    // Default move operations
+    SchedulerItem(SchedulerItem &&) = default;
+    SchedulerItem &operator=(SchedulerItem &&) = default;
+
+    // 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);
+  // Common implementation for cancel operations
+  bool cancel_item_common_(Component *component, bool is_static_string, const void *name_ptr, SchedulerItem::Type type);
+
+ private:
   bool cancel_item_(Component *component, const std::string &name, SchedulerItem::Type type);
+  bool cancel_item_(Component *component, const char *name, SchedulerItem::Type type);
+
+  // Helper functions for cancel operations
+  bool matches_item_(const std::unique_ptr &item, Component *component, const char *name_cstr,
+                     SchedulerItem::Type type);
+
+  // 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());
+  }
+
   bool empty_() {
     this->cleanup_();
     return this->items_.empty();
@@ -66,6 +167,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/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/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/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 ca987304e2..1826487aa4 100644
--- a/esphome/wizard.py
+++ b/esphome/wizard.py
@@ -67,20 +67,6 @@ esp8266:
 """
 
 ESP32_CONFIG = """
-esp32:
-  board: {board}
-  framework:
-    type: arduino
-"""
-
-ESP32S2_CONFIG = """
-esp32:
-  board: {board}
-  framework:
-    type: esp-idf
-"""
-
-ESP32C3_CONFIG = """
 esp32:
   board: {board}
   framework:
@@ -97,6 +83,11 @@ bk72xx:
   board: {board}
 """
 
+LN882X_CONFIG = """
+ln882x:
+  board: {board}
+"""
+
 RTL87XX_CONFIG = """
 rtl87xx:
   board: {board}
@@ -105,10 +96,9 @@ rtl87xx:
 HARDWARE_BASE_CONFIGS = {
     "ESP8266": ESP8266_CONFIG,
     "ESP32": ESP32_CONFIG,
-    "ESP32S2": ESP32S2_CONFIG,
-    "ESP32C3": ESP32C3_CONFIG,
     "RP2040": RP2040_CONFIG,
     "BK72XX": BK72XX_CONFIG,
+    "LN882X": LN882X_CONFIG,
     "RTL87XX": RTL87XX_CONFIG,
 }
 
@@ -173,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:
@@ -197,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
 
@@ -216,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:
@@ -269,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
 
@@ -341,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)."
@@ -377,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!")
@@ -400,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 78deec8e65..e52fc9e788 100644
--- a/esphome/yaml_util.py
+++ b/esphome/yaml_util.py
@@ -5,7 +5,7 @@ import fnmatch
 import functools
 import inspect
 from io import BytesIO, TextIOBase, TextIOWrapper
-from ipaddress import _BaseAddress
+from ipaddress import _BaseAddress, _BaseNetwork
 import logging
 import math
 import os
@@ -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):
@@ -621,6 +619,7 @@ ESPHomeDumper.add_multi_representer(str, ESPHomeDumper.represent_stringify)
 ESPHomeDumper.add_multi_representer(int, ESPHomeDumper.represent_int)
 ESPHomeDumper.add_multi_representer(float, ESPHomeDumper.represent_float)
 ESPHomeDumper.add_multi_representer(_BaseAddress, ESPHomeDumper.represent_stringify)
+ESPHomeDumper.add_multi_representer(_BaseNetwork, ESPHomeDumper.represent_stringify)
 ESPHomeDumper.add_multi_representer(MACAddress, ESPHomeDumper.represent_stringify)
 ESPHomeDumper.add_multi_representer(TimePeriod, ESPHomeDumper.represent_stringify)
 ESPHomeDumper.add_multi_representer(Lambda, ESPHomeDumper.represent_lambda)
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 d4bd0b7543..a6bcebaeea 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.1
+aioesphomeapi==34.1.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 d634be98c4..2266dda81c 100755
--- a/script/api_protobuf/api_protobuf.py
+++ b/script/api_protobuf/api_protobuf.py
@@ -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:
@@ -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:
@@ -848,7 +962,10 @@ def calculate_message_estimated_size(desc: descriptor.DescriptorProto) -> int:
     return total_size
 
 
-def build_message_type(desc: descriptor.DescriptorProto) -> tuple[str, str]:
+def build_message_type(
+    desc: descriptor.DescriptorProto,
+    base_class_fields: dict[str, list[descriptor.FieldDescriptorProto]] = None,
+) -> tuple[str, str, str]:
     public_content: list[str] = []
     protected_content: list[str] = []
     decode_varint: list[str] = []
@@ -859,6 +976,12 @@ def build_message_type(desc: descriptor.DescriptorProto) -> tuple[str, str]:
     dump: list[str] = []
     size_calc: list[str] = []
 
+    # Check if this message has a base class
+    base_class = get_base_class(desc)
+    common_field_names = set()
+    if base_class and base_class_fields and base_class in base_class_fields:
+        common_field_names = {f.name for f in base_class_fields[base_class]}
+
     # Get message ID if it's a service message
     message_id: int | None = get_opt(desc, pb.id)
 
@@ -873,11 +996,11 @@ def build_message_type(desc: descriptor.DescriptorProto) -> tuple[str, str]:
             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")
 
@@ -886,8 +1009,14 @@ def build_message_type(desc: descriptor.DescriptorProto) -> tuple[str, str]:
             ti = RepeatedTypeInfo(field)
         else:
             ti = TYPE_INFO[field.type](field)
-        protected_content.extend(ti.protected_content)
-        public_content.extend(ti.public_content)
+
+        # Skip field declarations for fields that are in the base class
+        # but include their encode/decode logic
+        if field.name not in common_field_names:
+            protected_content.extend(ti.protected_content)
+            public_content.extend(ti.public_content)
+
+        # Always include encode/decode logic for all fields
         encode.append(ti.encode_content)
         size_calc.append(ti.get_size_calculation(f"this->{ti.field_name}"))
 
@@ -944,64 +1073,66 @@ def build_message_type(desc: descriptor.DescriptorProto) -> tuple[str, str]:
         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)
 
-    out = f"class {desc.name} : public ProtoMessage {{\n"
+    # 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:
+        out = f"class {desc.name} : public ProtoMessage {{\n"
     out += " public:\n"
     out += indent("\n".join(public_content)) + "\n"
     out += "\n"
@@ -1010,14 +1141,18 @@ def build_message_type(desc: descriptor.DescriptorProto) -> tuple[str, str]:
     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] = {}
 
@@ -1033,6 +1168,135 @@ def get_opt(
     return desc.options.Extensions[opt]
 
 
+def get_base_class(desc: descriptor.DescriptorProto) -> str | None:
+    """Get the base_class option from a message descriptor."""
+    if not desc.options.HasExtension(pb.base_class):
+        return None
+    return desc.options.Extensions[pb.base_class]
+
+
+def collect_messages_by_base_class(
+    messages: list[descriptor.DescriptorProto],
+) -> dict[str, list[descriptor.DescriptorProto]]:
+    """Group messages by their base_class option."""
+    base_class_groups = {}
+
+    for msg in messages:
+        base_class = get_base_class(msg)
+        if base_class:
+            if base_class not in base_class_groups:
+                base_class_groups[base_class] = []
+            base_class_groups[base_class].append(msg)
+
+    return base_class_groups
+
+
+def find_common_fields(
+    messages: list[descriptor.DescriptorProto],
+) -> list[descriptor.FieldDescriptorProto]:
+    """Find fields that are common to all messages in the list."""
+    if not messages:
+        return []
+
+    # Start with fields from the first message
+    first_msg_fields = {field.name: field for field in messages[0].field}
+    common_fields = []
+
+    # Check each field to see if it exists in all messages with same type
+    # Field numbers can vary between messages - derived classes handle the mapping
+    for field_name, field in first_msg_fields.items():
+        is_common = True
+
+        for msg in messages[1:]:
+            found = False
+            for other_field in msg.field:
+                if (
+                    other_field.name == field_name
+                    and other_field.type == field.type
+                    and other_field.label == field.label
+                ):
+                    found = True
+                    break
+
+            if not found:
+                is_common = False
+                break
+
+        if is_common:
+            common_fields.append(field)
+
+    # Sort by field number to maintain order
+    common_fields.sort(key=lambda f: f.number)
+    return common_fields
+
+
+def build_base_class(
+    base_class_name: str,
+    common_fields: list[descriptor.FieldDescriptorProto],
+) -> tuple[str, str, str]:
+    """Build the base class definition and implementation."""
+    public_content = []
+    protected_content = []
+
+    # For base classes, we only declare the fields but don't handle encode/decode
+    # The derived classes will handle encoding/decoding with their specific field numbers
+    for field in common_fields:
+        if field.label == 3:  # repeated
+            ti = RepeatedTypeInfo(field)
+        else:
+            ti = TYPE_INFO[field.type](field)
+
+        # Only add field declarations, not encode/decode logic
+        protected_content.extend(ti.protected_content)
+        public_content.extend(ti.public_content)
+
+    # Build header
+    out = f"class {base_class_name} : public ProtoMessage {{\n"
+    out += " public:\n"
+
+    # Add destructor with override
+    public_content.insert(0, f"~{base_class_name}() override = default;")
+
+    # Base classes don't implement encode/decode/calculate_size
+    # Derived classes handle these with their specific field numbers
+    cpp = ""
+
+    out += indent("\n".join(public_content)) + "\n"
+    out += "\n"
+    out += " protected:\n"
+    out += indent("\n".join(protected_content))
+    if protected_content:
+        out += "\n"
+    out += "};\n"
+
+    # No implementation needed for base classes
+    dump_cpp = ""
+
+    return out, cpp, dump_cpp
+
+
+def generate_base_classes(
+    base_class_groups: dict[str, list[descriptor.DescriptorProto]],
+) -> 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
+        common_fields = find_common_fields(messages)
+
+        if common_fields:
+            # Generate base class
+            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), "\n".join(all_dump_cpp)
+
+
 def build_service_message_type(
     mt: descriptor.DescriptorProto,
 ) -> tuple[str, str] | None:
@@ -1065,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:
@@ -1074,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:
@@ -1100,55 +1361,147 @@ 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"
 
     mt = file.message_type
 
+    # Collect messages by base class
+    base_class_groups = collect_messages_by_base_class(mt)
+
+    # Find common fields for each base class
+    base_class_fields = {}
+    for base_class_name, messages in base_class_groups.items():
+        common_fields = find_common_fields(messages)
+        if common_fields:
+            base_class_fields[base_class_name] = common_fields
+
+    # Generate base classes
+    if base_class_fields:
+        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)
+        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)
@@ -1156,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"
 
@@ -1196,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"
@@ -1219,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"
@@ -1254,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"
@@ -1264,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:
@@ -1296,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)
@@ -1326,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/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/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/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/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 d8452bdd2a..2edc62b6a1 100644
--- a/tests/components/lvgl/lvgl-package.yaml
+++ b/tests/components/lvgl/lvgl-package.yaml
@@ -646,7 +646,9 @@ lvgl:
             on_click:
               lvgl.qrcode.update:
                 id: lv_qr
-                text: homeassistant.io
+                text:
+                  format: "A string with a number %d"
+                  args: ['(int)(random_uint32() % 1000)']
 
         - slider:
             min_value: 0
@@ -728,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]
@@ -834,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 482fd1a453..bbcf48efa5 100644
--- a/tests/components/openthread/test.esp32-c6-idf.yaml
+++ b/tests/components/openthread/test.esp32-c6-idf.yaml
@@ -2,10 +2,13 @@ network:
   enable_ipv6: true
 
 openthread:
+  device_type: FTD
   channel: 13
   network_name: OpenThread-8f28
   network_key: 0xdfd34f0f05cad978ec4e32b0413038ff
   pan_id: 0x8f28
   ext_pan_id: 0xd63e8e3e495ebbc3
   pskc: 0xc23a76e98f1a6483639b1ac1271e2e27
+  mesh_local_prefix: fd53:145f:ed22:ad81::/64
   force_dataset: true
+
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/conftest.py b/tests/integration/conftest.py
index 4c798c6b72..8f5f77ca52 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
@@ -15,7 +15,7 @@ import sys
 import tempfile
 from typing import TextIO
 
-from aioesphomeapi import APIClient, APIConnectionError, ReconnectLogic
+from aioesphomeapi import APIClient, APIConnectionError, LogParser, ReconnectLogic
 import pytest
 import pytest_asyncio
 
@@ -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
 
 
@@ -119,6 +120,21 @@ async def yaml_config(request: pytest.FixtureRequest, unused_tcp_port: int) -> s
         # Add port configuration after api:
         content = content.replace("api:", f"api:\n  port: {unused_tcp_port}")
 
+    # Add debug build flags for integration tests to enable assertions
+    if "esphome:" in content:
+        # Check if platformio_options already exists
+        if "platformio_options:" not in content:
+            # Add platformio_options with debug flags after esphome:
+            content = content.replace(
+                "esphome:",
+                "esphome:\n"
+                "  # Enable assertions for integration tests\n"
+                "  platformio_options:\n"
+                "    build_flags:\n"
+                '      - "-DDEBUG"  # Enable assert() statements\n'
+                '      - "-g"       # Add debug symbols',
+            )
+
     return content
 
 
@@ -187,6 +203,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)}
@@ -347,14 +364,30 @@ 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()
     while line := await stream.readline():
-        decoded_line = line.decode("utf-8", errors="replace")
+        decoded_line = (
+            line.replace(b"\r", b"")
+            .replace(b"\n", b"")
+            .decode("utf8", "backslashreplace")
+        )
         lines.append(decoded_line.rstrip())
         # Echo to stdout/stderr in real-time
-        print(decoded_line.rstrip(), file=output_stream, flush=True)
+        # Print without newline to avoid double newlines
+        print(
+            log_parser.parse_line(decoded_line, timestamp=""),
+            file=output_stream,
+            flush=True,
+        )
+        # Call the callback if provided
+        if line_callback:
+            line_callback(decoded_line.rstrip())
 
 
 @asynccontextmanager
@@ -363,6 +396,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
@@ -410,7 +444,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
+                )
             )
         ]
 
@@ -490,6 +526,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
@@ -503,7 +540,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
 
 
@@ -517,7 +556,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,
@@ -526,6 +567,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..4bbba5084b
--- /dev/null
+++ b/tests/integration/fixtures/api_conditional_memory.yaml
@@ -0,0 +1,71 @@
+esphome:
+  name: api-conditional-memory-test
+host:
+api:
+  actions:
+    - action: test_simple_service
+      then:
+        - logger.log: "Simple service called"
+        - binary_sensor.template.publish:
+            id: service_called_sensor
+            state: ON
+    - 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]
+        - sensor.template.publish:
+            id: service_arg_sensor
+            state: !lambda 'return arg_float;'
+  on_client_connected:
+    - logger.log:
+        format: "Client %s connected from %s"
+        args: [client_info.c_str(), client_address.c_str()]
+    - binary_sensor.template.publish:
+        id: client_connected
+        state: ON
+    - text_sensor.template.publish:
+        id: last_client_info
+        state: !lambda 'return client_info;'
+  on_client_disconnected:
+    - logger.log:
+        format: "Client %s disconnected from %s"
+        args: [client_info.c_str(), client_address.c_str()]
+    - binary_sensor.template.publish:
+        id: client_connected
+        state: OFF
+    - binary_sensor.template.publish:
+        id: client_disconnected_event
+        state: ON
+
+logger:
+  level: DEBUG
+
+binary_sensor:
+  - platform: template
+    name: "Client Connected"
+    id: client_connected
+    device_class: connectivity
+  - platform: template
+    name: "Client Disconnected Event"
+    id: client_disconnected_event
+  - platform: template
+    name: "Service Called"
+    id: service_called_sensor
+
+sensor:
+  - platform: template
+    name: "Service Argument Value"
+    id: service_arg_sensor
+    unit_of_measurement: ""
+    accuracy_decimals: 2
+
+text_sensor:
+  - platform: template
+    name: "Last Client Info"
+    id: last_client_info
diff --git a/tests/integration/fixtures/api_message_size_batching.yaml b/tests/integration/fixtures/api_message_size_batching.yaml
new file mode 100644
index 0000000000..c730dc1aa3
--- /dev/null
+++ b/tests/integration/fixtures/api_message_size_batching.yaml
@@ -0,0 +1,161 @@
+esphome:
+  name: message-size-batching-test
+host:
+api:
+# Default batch_delay to test batching
+logger:
+
+# Create entities that will produce different protobuf header sizes
+# Header size depends on: 1 byte indicator + varint(payload_size) + varint(message_type)
+# 4-byte header: type < 128, payload < 128
+# 5-byte header: type < 128, payload 128-16383 OR type 128+, payload < 128
+# 6-byte header: type 128+, payload 128-16383
+
+# Small select with few options - produces small message
+select:
+  - platform: template
+    name: "Small Select"
+    id: small_select
+    optimistic: true
+    options:
+      - "Option A"
+      - "Option B"
+    initial_option: "Option A"
+    update_interval: 5.0s
+
+  # Medium select with more options - produces medium message
+  - platform: template
+    name: "Medium Select"
+    id: medium_select
+    optimistic: true
+    options:
+      - "Option 001"
+      - "Option 002"
+      - "Option 003"
+      - "Option 004"
+      - "Option 005"
+      - "Option 006"
+      - "Option 007"
+      - "Option 008"
+      - "Option 009"
+      - "Option 010"
+      - "Option 011"
+      - "Option 012"
+      - "Option 013"
+      - "Option 014"
+      - "Option 015"
+      - "Option 016"
+      - "Option 017"
+      - "Option 018"
+      - "Option 019"
+      - "Option 020"
+    initial_option: "Option 001"
+    update_interval: 5.0s
+
+  # Large select with many options - produces larger message
+  - platform: template
+    name: "Large Select with Many Options to Create Larger Payload"
+    id: large_select
+    optimistic: true
+    options:
+      - "Long Option Name 001 - This is a longer option name to increase message size"
+      - "Long Option Name 002 - This is a longer option name to increase message size"
+      - "Long Option Name 003 - This is a longer option name to increase message size"
+      - "Long Option Name 004 - This is a longer option name to increase message size"
+      - "Long Option Name 005 - This is a longer option name to increase message size"
+      - "Long Option Name 006 - This is a longer option name to increase message size"
+      - "Long Option Name 007 - This is a longer option name to increase message size"
+      - "Long Option Name 008 - This is a longer option name to increase message size"
+      - "Long Option Name 009 - This is a longer option name to increase message size"
+      - "Long Option Name 010 - This is a longer option name to increase message size"
+      - "Long Option Name 011 - This is a longer option name to increase message size"
+      - "Long Option Name 012 - This is a longer option name to increase message size"
+      - "Long Option Name 013 - This is a longer option name to increase message size"
+      - "Long Option Name 014 - This is a longer option name to increase message size"
+      - "Long Option Name 015 - This is a longer option name to increase message size"
+      - "Long Option Name 016 - This is a longer option name to increase message size"
+      - "Long Option Name 017 - This is a longer option name to increase message size"
+      - "Long Option Name 018 - This is a longer option name to increase message size"
+      - "Long Option Name 019 - This is a longer option name to increase message size"
+      - "Long Option Name 020 - This is a longer option name to increase message size"
+      - "Long Option Name 021 - This is a longer option name to increase message size"
+      - "Long Option Name 022 - This is a longer option name to increase message size"
+      - "Long Option Name 023 - This is a longer option name to increase message size"
+      - "Long Option Name 024 - This is a longer option name to increase message size"
+      - "Long Option Name 025 - This is a longer option name to increase message size"
+      - "Long Option Name 026 - This is a longer option name to increase message size"
+      - "Long Option Name 027 - This is a longer option name to increase message size"
+      - "Long Option Name 028 - This is a longer option name to increase message size"
+      - "Long Option Name 029 - This is a longer option name to increase message size"
+      - "Long Option Name 030 - This is a longer option name to increase message size"
+      - "Long Option Name 031 - This is a longer option name to increase message size"
+      - "Long Option Name 032 - This is a longer option name to increase message size"
+      - "Long Option Name 033 - This is a longer option name to increase message size"
+      - "Long Option Name 034 - This is a longer option name to increase message size"
+      - "Long Option Name 035 - This is a longer option name to increase message size"
+      - "Long Option Name 036 - This is a longer option name to increase message size"
+      - "Long Option Name 037 - This is a longer option name to increase message size"
+      - "Long Option Name 038 - This is a longer option name to increase message size"
+      - "Long Option Name 039 - This is a longer option name to increase message size"
+      - "Long Option Name 040 - This is a longer option name to increase message size"
+      - "Long Option Name 041 - This is a longer option name to increase message size"
+      - "Long Option Name 042 - This is a longer option name to increase message size"
+      - "Long Option Name 043 - This is a longer option name to increase message size"
+      - "Long Option Name 044 - This is a longer option name to increase message size"
+      - "Long Option Name 045 - This is a longer option name to increase message size"
+      - "Long Option Name 046 - This is a longer option name to increase message size"
+      - "Long Option Name 047 - This is a longer option name to increase message size"
+      - "Long Option Name 048 - This is a longer option name to increase message size"
+      - "Long Option Name 049 - This is a longer option name to increase message size"
+      - "Long Option Name 050 - This is a longer option name to increase message size"
+    initial_option: "Long Option Name 001 - This is a longer option name to increase message size"
+    update_interval: 5.0s
+
+# Text sensors with different value lengths
+text_sensor:
+  - platform: template
+    name: "Short Text Sensor"
+    id: short_text_sensor
+    lambda: |-
+      return {"OK"};
+    update_interval: 5.0s
+
+  - platform: template
+    name: "Medium Text Sensor"
+    id: medium_text_sensor
+    lambda: |-
+      return {"This is a medium length text sensor value that should produce a medium sized message"};
+    update_interval: 5.0s
+
+  - platform: template
+    name: "Long Text Sensor with Very Long Value"
+    id: long_text_sensor
+    lambda: |-
+      return {"This is a very long text sensor value that contains a lot of text to ensure we get a larger protobuf message. The message should be long enough to require a 2-byte varint for the payload size, which happens when the payload exceeds 127 bytes. Let's add even more text here to make sure we exceed that threshold and test the batching of messages with different header sizes properly."};
+    update_interval: 5.0s
+
+# Text input which can have various lengths
+text:
+  - platform: template
+    name: "Test Text Input"
+    id: test_text_input
+    optimistic: true
+    mode: text
+    min_length: 0
+    max_length: 255
+    initial_value: "Initial value"
+    update_interval: 5.0s
+
+# Number entity to add variety (different message type number)
+# The ListEntitiesNumberResponse has message type 49
+# The NumberStateResponse has message type 50
+number:
+  - platform: template
+    name: "Test Number with Long Name to Increase Message Size"
+    id: test_number
+    optimistic: true
+    min_value: 0
+    max_value: 1000
+    step: 0.1
+    initial_value: 42.0
+    update_interval: 5.0s
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/defer_fifo_simple.yaml b/tests/integration/fixtures/defer_fifo_simple.yaml
new file mode 100644
index 0000000000..db24ebf601
--- /dev/null
+++ b/tests/integration/fixtures/defer_fifo_simple.yaml
@@ -0,0 +1,109 @@
+esphome:
+  name: 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/defer_stress.yaml b/tests/integration/fixtures/defer_stress.yaml
new file mode 100644
index 0000000000..6df475229b
--- /dev/null
+++ b/tests/integration/fixtures/defer_stress.yaml
@@ -0,0 +1,38 @@
+esphome:
+  name: 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/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/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/host_mode_empty_string_options.yaml b/tests/integration/fixtures/host_mode_empty_string_options.yaml
new file mode 100644
index 0000000000..ab8e6cd005
--- /dev/null
+++ b/tests/integration/fixtures/host_mode_empty_string_options.yaml
@@ -0,0 +1,58 @@
+esphome:
+  name: host-empty-string-test
+
+host:
+
+api:
+  batch_delay: 50ms
+
+select:
+  - platform: template
+    name: "Select Empty First"
+    id: select_empty_first
+    optimistic: true
+    options:
+      - ""  # Empty string at the beginning
+      - "Option A"
+      - "Option B"
+      - "Option C"
+    initial_option: "Option A"
+
+  - platform: template
+    name: "Select Empty Middle"
+    id: select_empty_middle
+    optimistic: true
+    options:
+      - "Option 1"
+      - "Option 2"
+      - ""  # Empty string in the middle
+      - "Option 3"
+      - "Option 4"
+    initial_option: "Option 1"
+
+  - platform: template
+    name: "Select Empty Last"
+    id: select_empty_last
+    optimistic: true
+    options:
+      - "Choice X"
+      - "Choice Y"
+      - "Choice Z"
+      - ""  # Empty string at the end
+    initial_option: "Choice X"
+
+# Add a sensor to ensure we have other entities in the list
+sensor:
+  - platform: template
+    name: "Test Sensor"
+    id: test_sensor
+    lambda: |-
+      return 42.0;
+    update_interval: 60s
+
+binary_sensor:
+  - platform: template
+    name: "Test Binary Sensor"
+    id: test_binary_sensor
+    lambda: |-
+      return true;
diff --git a/tests/integration/fixtures/host_mode_entity_fields.yaml b/tests/integration/fixtures/host_mode_entity_fields.yaml
new file mode 100644
index 0000000000..0bd87ee794
--- /dev/null
+++ b/tests/integration/fixtures/host_mode_entity_fields.yaml
@@ -0,0 +1,108 @@
+esphome:
+  name: host-test
+
+host:
+
+api:
+
+logger:
+
+# Test various entity types with different flag combinations
+sensor:
+  - platform: template
+    name: "Test Normal Sensor"
+    id: normal_sensor
+    update_interval: 1s
+    lambda: |-
+      return 42.0;
+
+  - platform: template
+    name: "Test Internal Sensor"
+    id: internal_sensor
+    internal: true
+    update_interval: 1s
+    lambda: |-
+      return 43.0;
+
+  - platform: template
+    name: "Test Disabled Sensor"
+    id: disabled_sensor
+    disabled_by_default: true
+    update_interval: 1s
+    lambda: |-
+      return 44.0;
+
+  - platform: template
+    name: "Test Mixed Flags Sensor"
+    id: mixed_flags_sensor
+    internal: true
+    entity_category: diagnostic
+    update_interval: 1s
+    lambda: |-
+      return 45.0;
+
+  - platform: template
+    name: "Test Diagnostic Sensor"
+    id: diagnostic_sensor
+    entity_category: diagnostic
+    update_interval: 1s
+    lambda: |-
+      return 46.0;
+
+  - platform: template
+    name: "Test All Flags Sensor"
+    id: all_flags_sensor
+    internal: true
+    disabled_by_default: true
+    entity_category: diagnostic
+    update_interval: 1s
+    lambda: |-
+      return 47.0;
+
+# Also test other entity types to ensure bit-packing works across all
+binary_sensor:
+  - platform: template
+    name: "Test Binary Sensor"
+    entity_category: config
+    lambda: |-
+      return true;
+
+text_sensor:
+  - platform: template
+    name: "Test Text Sensor"
+    disabled_by_default: true
+    lambda: |-
+      return {"Hello"};
+
+number:
+  - platform: template
+    name: "Test Number"
+    initial_value: 50
+    min_value: 0
+    max_value: 100
+    step: 1
+    optimistic: true
+    entity_category: diagnostic
+
+select:
+  - platform: template
+    name: "Test Select"
+    options:
+      - "Option 1"
+      - "Option 2"
+    initial_option: "Option 1"
+    optimistic: true
+    internal: true
+
+switch:
+  - platform: template
+    name: "Test Switch"
+    optimistic: true
+    disabled_by_default: true
+    entity_category: config
+
+button:
+  - platform: template
+    name: "Test Button"
+    on_press:
+      - logger.log: "Button pressed"
diff --git a/tests/integration/fixtures/host_mode_fan_preset.yaml b/tests/integration/fixtures/host_mode_fan_preset.yaml
new file mode 100644
index 0000000000..003f4a7760
--- /dev/null
+++ b/tests/integration/fixtures/host_mode_fan_preset.yaml
@@ -0,0 +1,34 @@
+esphome:
+  name: host-test
+
+host:
+
+api:
+
+logger:
+
+# Test fan with preset modes and speed settings
+fan:
+  - platform: template
+    name: "Test Fan with Presets"
+    id: test_fan_presets
+    speed_count: 5
+    preset_modes:
+      - "Eco"
+      - "Sleep"
+      - "Turbo"
+    has_oscillating: true
+    has_direction: true
+
+  - platform: template
+    name: "Test Fan Simple"
+    id: test_fan_simple
+    speed_count: 3
+    has_oscillating: false
+    has_direction: false
+
+  - platform: template
+    name: "Test Fan No Speed"
+    id: test_fan_no_speed
+    has_oscillating: true
+    has_direction: false
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/large_message_batching.yaml b/tests/integration/fixtures/large_message_batching.yaml
new file mode 100644
index 0000000000..1b2d817cd4
--- /dev/null
+++ b/tests/integration/fixtures/large_message_batching.yaml
@@ -0,0 +1,137 @@
+esphome:
+  name: large-message-test
+host:
+api:
+logger:
+
+# Create a select entity with many options to exceed 1390 bytes
+select:
+  - platform: template
+    name: "Large Select"
+    id: large_select
+    optimistic: true
+    options:
+      - "Option 000 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 001 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 002 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 003 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 004 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 005 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 006 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 007 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 008 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 009 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 010 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 011 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 012 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 013 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 014 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 015 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 016 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 017 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 018 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 019 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 020 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 021 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 022 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 023 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 024 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 025 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 026 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 027 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 028 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 029 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 030 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 031 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 032 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 033 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 034 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 035 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 036 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 037 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 038 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 039 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 040 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 041 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 042 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 043 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 044 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 045 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 046 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 047 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 048 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 049 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 050 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 051 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 052 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 053 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 054 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 055 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 056 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 057 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 058 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 059 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 060 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 061 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 062 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 063 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 064 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 065 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 066 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 067 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 068 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 069 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 070 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 071 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 072 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 073 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 074 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 075 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 076 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 077 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 078 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 079 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 080 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 081 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 082 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 083 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 084 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 085 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 086 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 087 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 088 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 089 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 090 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 091 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 092 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 093 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 094 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 095 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 096 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 097 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 098 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+      - "Option 099 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+    initial_option: "Option 000 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+
+# Add some other entities to test batching with the large select
+sensor:
+  - platform: template
+    name: "Test Sensor"
+    id: test_sensor
+    lambda: |-
+      return 42.0;
+    update_interval: 1s
+
+binary_sensor:
+  - platform: template
+    name: "Test Binary Sensor"
+    id: test_binary_sensor
+    lambda: |-
+      return true;
+
+switch:
+  - platform: template
+    name: "Test Switch"
+    id: test_switch
+    optimistic: true
+
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/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_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..8048624f70
--- /dev/null
+++ b/tests/integration/test_api_conditional_memory.py
@@ -0,0 +1,208 @@
+"""Integration test for API conditional memory optimization with triggers and services."""
+
+from __future__ import annotations
+
+import asyncio
+
+from aioesphomeapi import (
+    BinarySensorInfo,
+    EntityState,
+    SensorInfo,
+    TextSensorInfo,
+    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()
+    # Keep ESPHome process running throughout the test
+    async with run_compiled(yaml_config):
+        # First connection
+        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"
+
+            # List entities and services
+            entity_info, services = await asyncio.wait_for(
+                client.list_entities_services(), timeout=5.0
+            )
+
+            # Find our entities
+            client_connected: BinarySensorInfo | None = None
+            client_disconnected_event: BinarySensorInfo | None = None
+            service_called_sensor: BinarySensorInfo | None = None
+            service_arg_sensor: SensorInfo | None = None
+            last_client_info: TextSensorInfo | None = None
+
+            for entity in entity_info:
+                if isinstance(entity, BinarySensorInfo):
+                    if entity.object_id == "client_connected":
+                        client_connected = entity
+                    elif entity.object_id == "client_disconnected_event":
+                        client_disconnected_event = entity
+                    elif entity.object_id == "service_called":
+                        service_called_sensor = entity
+                elif isinstance(entity, SensorInfo):
+                    if entity.object_id == "service_argument_value":
+                        service_arg_sensor = entity
+                elif isinstance(entity, TextSensorInfo):
+                    if entity.object_id == "last_client_info":
+                        last_client_info = entity
+
+            # Verify all entities exist
+            assert client_connected is not None, "client_connected sensor not found"
+            assert client_disconnected_event is not None, (
+                "client_disconnected_event sensor not found"
+            )
+            assert service_called_sensor is not None, "service_called sensor not found"
+            assert service_arg_sensor is not None, "service_arg_sensor not found"
+            assert last_client_info is not None, "last_client_info sensor not found"
+
+            # 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
+
+            # Track state changes
+            states: dict[int, EntityState] = {}
+            states_future: asyncio.Future[None] = loop.create_future()
+
+            def on_state(state: EntityState) -> None:
+                states[state.key] = state
+                # Check if we have initial states for connection sensors
+                if (
+                    client_connected.key in states
+                    and last_client_info.key in states
+                    and not states_future.done()
+                ):
+                    states_future.set_result(None)
+
+            client.subscribe_states(on_state)
+
+            # Wait for initial states
+            await asyncio.wait_for(states_future, timeout=5.0)
+
+            # Verify on_client_connected trigger fired
+            connected_state = states.get(client_connected.key)
+            assert connected_state is not None
+            assert connected_state.state is True, "Client should be connected"
+
+            # Verify client info was captured
+            client_info_state = states.get(last_client_info.key)
+            assert client_info_state is not None
+            assert isinstance(client_info_state.state, str)
+            assert len(client_info_state.state) > 0, "Client info should not be empty"
+
+            # Test simple service
+            service_future: asyncio.Future[None] = loop.create_future()
+
+            def check_service_called(state: EntityState) -> None:
+                if state.key == service_called_sensor.key and state.state is True:
+                    if not service_future.done():
+                        service_future.set_result(None)
+
+            # Update callback to check for service execution
+            client.subscribe_states(check_service_called)
+
+            # Call simple service
+            client.execute_service(simple_service, {})
+
+            # Wait for service to execute
+            await asyncio.wait_for(service_future, timeout=5.0)
+
+            # Test service with arguments
+            arg_future: asyncio.Future[None] = loop.create_future()
+            expected_float = 42.5
+
+            def check_arg_sensor(state: EntityState) -> None:
+                if (
+                    state.key == service_arg_sensor.key
+                    and abs(state.state - expected_float) < 0.01
+                ):
+                    if not arg_future.done():
+                        arg_future.set_result(None)
+
+            client.subscribe_states(check_arg_sensor)
+
+            # Call service with arguments
+            client.execute_service(
+                service_with_args,
+                {
+                    "arg_string": "test_string",
+                    "arg_int": 123,
+                    "arg_bool": True,
+                    "arg_float": expected_float,
+                },
+            )
+
+            # Wait for service with args to execute
+            await asyncio.wait_for(arg_future, timeout=5.0)
+
+        # After disconnecting first client, reconnect and verify triggers work
+        async with api_client_connected() as client2:
+            # Subscribe to states with new client
+            states2: dict[int, EntityState] = {}
+            states_ready_future: asyncio.Future[None] = loop.create_future()
+
+            def on_state2(state: EntityState) -> None:
+                states2[state.key] = state
+                # Check if we have received both required states
+                if (
+                    client_connected.key in states2
+                    and client_disconnected_event.key in states2
+                    and not states_ready_future.done()
+                ):
+                    states_ready_future.set_result(None)
+
+            client2.subscribe_states(on_state2)
+
+            # Wait for both connected and disconnected event states
+            await asyncio.wait_for(states_ready_future, timeout=5.0)
+
+            # Verify client is connected again (on_client_connected fired)
+            assert states2[client_connected.key].state is True, (
+                "Client should be reconnected"
+            )
+
+            # The client_disconnected_event should be ON from when we disconnected
+            # (it was set ON by on_client_disconnected trigger)
+            disconnected_state = states2.get(client_disconnected_event.key)
+            assert disconnected_state is not None
+            assert disconnected_state.state is True, (
+                "Disconnect event should be ON from previous disconnect"
+            )
diff --git a/tests/integration/test_api_message_size_batching.py b/tests/integration/test_api_message_size_batching.py
new file mode 100644
index 0000000000..631e64825e
--- /dev/null
+++ b/tests/integration/test_api_message_size_batching.py
@@ -0,0 +1,194 @@
+"""Integration test for API batching with various message sizes."""
+
+from __future__ import annotations
+
+import asyncio
+
+from aioesphomeapi import EntityState, NumberInfo, SelectInfo, TextInfo, TextSensorInfo
+import pytest
+
+from .types import APIClientConnectedFactory, RunCompiledFunction
+
+
+@pytest.mark.asyncio
+async def test_api_message_size_batching(
+    yaml_config: str,
+    run_compiled: RunCompiledFunction,
+    api_client_connected: APIClientConnectedFactory,
+) -> None:
+    """Test API can batch messages of various sizes correctly."""
+    # 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:
+        # Verify we can get device info
+        device_info = await client.device_info()
+        assert device_info is not None
+        assert device_info.name == "message-size-batching-test"
+
+        # List entities - this will batch various sized messages together
+        entity_info, services = await asyncio.wait_for(
+            client.list_entities_services(), timeout=5.0
+        )
+
+        # Count different entity types
+        selects = []
+        text_sensors = []
+        text_inputs = []
+        numbers = []
+        other_entities = []
+
+        for entity in entity_info:
+            if isinstance(entity, SelectInfo):
+                selects.append(entity)
+            elif isinstance(entity, TextSensorInfo):
+                text_sensors.append(entity)
+            elif isinstance(entity, TextInfo):
+                text_inputs.append(entity)
+            elif isinstance(entity, NumberInfo):
+                numbers.append(entity)
+            else:
+                other_entities.append(entity)
+
+        # Verify we have our test entities - exact counts
+        assert len(selects) == 3, (
+            f"Expected exactly 3 select entities, got {len(selects)}"
+        )
+        assert len(text_sensors) == 3, (
+            f"Expected exactly 3 text sensor entities, got {len(text_sensors)}"
+        )
+        assert len(text_inputs) == 1, (
+            f"Expected exactly 1 text input entity, got {len(text_inputs)}"
+        )
+
+        # Collect all select entity object_ids for error messages
+        select_ids = [s.object_id for s in selects]
+
+        # Find our specific test entities
+        small_select = None
+        medium_select = None
+        large_select = None
+
+        for select in selects:
+            if select.object_id == "small_select":
+                small_select = select
+            elif select.object_id == "medium_select":
+                medium_select = select
+            elif (
+                select.object_id
+                == "large_select_with_many_options_to_create_larger_payload"
+            ):
+                large_select = select
+
+        assert small_select is not None, (
+            f"Could not find small_select entity. Found: {select_ids}"
+        )
+        assert medium_select is not None, (
+            f"Could not find medium_select entity. Found: {select_ids}"
+        )
+        assert large_select is not None, (
+            f"Could not find large_select entity. Found: {select_ids}"
+        )
+
+        # Verify the selects have the expected number of options
+        assert len(small_select.options) == 2, (
+            f"Expected 2 options for small_select, got {len(small_select.options)}"
+        )
+        assert len(medium_select.options) == 20, (
+            f"Expected 20 options for medium_select, got {len(medium_select.options)}"
+        )
+        assert len(large_select.options) == 50, (
+            f"Expected 50 options for large_select, got {len(large_select.options)}"
+        )
+
+        # Collect all text sensor object_ids for error messages
+        text_sensor_ids = [t.object_id for t in text_sensors]
+
+        # Verify text sensors with different value lengths
+        short_text_sensor = None
+        medium_text_sensor = None
+        long_text_sensor = None
+
+        for text_sensor in text_sensors:
+            if text_sensor.object_id == "short_text_sensor":
+                short_text_sensor = text_sensor
+            elif text_sensor.object_id == "medium_text_sensor":
+                medium_text_sensor = text_sensor
+            elif text_sensor.object_id == "long_text_sensor_with_very_long_value":
+                long_text_sensor = text_sensor
+
+        assert short_text_sensor is not None, (
+            f"Could not find short_text_sensor. Found: {text_sensor_ids}"
+        )
+        assert medium_text_sensor is not None, (
+            f"Could not find medium_text_sensor. Found: {text_sensor_ids}"
+        )
+        assert long_text_sensor is not None, (
+            f"Could not find long_text_sensor. Found: {text_sensor_ids}"
+        )
+
+        # Check text input which can have a long max_length
+        text_input = None
+        text_input_ids = [t.object_id for t in text_inputs]
+
+        for ti in text_inputs:
+            if ti.object_id == "test_text_input":
+                text_input = ti
+                break
+
+        assert text_input is not None, (
+            f"Could not find test_text_input. Found: {text_input_ids}"
+        )
+        assert text_input.max_length == 255, (
+            f"Expected max_length 255, got {text_input.max_length}"
+        )
+
+        # Verify total entity count - messages of various sizes were batched successfully
+        # We have: 3 selects + 3 text sensors + 1 text input + 1 number = 8 total
+        total_entities = len(entity_info)
+        assert total_entities == 8, f"Expected exactly 8 entities, got {total_entities}"
+
+        # Check we have the expected entity types
+        assert len(numbers) == 1, (
+            f"Expected exactly 1 number entity, got {len(numbers)}"
+        )
+        assert len(other_entities) == 0, (
+            f"Unexpected entity types found: {[type(e).__name__ for e in other_entities]}"
+        )
+
+        # Subscribe to state changes to verify batching works
+        # Collect keys from entity info to know what states to expect
+        expected_keys = {entity.key for entity in entity_info}
+        assert len(expected_keys) == 8, (
+            f"Expected 8 unique entity keys, got {len(expected_keys)}"
+        )
+
+        received_keys: set[int] = set()
+        states_future: asyncio.Future[None] = loop.create_future()
+
+        def on_state(state: EntityState) -> None:
+            """Track when states are received."""
+            received_keys.add(state.key)
+            # Check if we've received states from all expected entities
+            if expected_keys.issubset(received_keys) and not states_future.done():
+                states_future.set_result(None)
+
+        client.subscribe_states(on_state)
+
+        # Wait for states with timeout
+        try:
+            await asyncio.wait_for(states_future, timeout=5.0)
+        except asyncio.TimeoutError:
+            missing_keys = expected_keys - received_keys
+            pytest.fail(
+                f"Did not receive states from all entities within 5 seconds. "
+                f"Missing keys: {missing_keys}, "
+                f"Received {len(received_keys)} of {len(expected_keys)} expected states"
+            )
+
+        # Verify we received states from all entities
+        assert expected_keys.issubset(received_keys)
+
+        # Check that various message sizes were handled correctly
+        # Small messages (4-byte header): type < 128, payload < 128
+        # Medium messages (5-byte header): type < 128, payload 128-16383 OR type 128+, payload < 128
+        # Large messages (6-byte header): type 128+, payload 128-16383
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..19aab2001c
--- /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
+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 hasattr(e, "unit_of_measurement"))
+        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..4ce55a30a7
--- /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
+        sensor_entities = [e for e in entities[0] if hasattr(e, "device_id")]
+        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_defer_fifo_simple.py b/tests/integration/test_defer_fifo_simple.py
new file mode 100644
index 0000000000..5a62a45786
--- /dev/null
+++ b/tests/integration/test_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_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 == "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_defer_stress.py b/tests/integration/test_defer_stress.py
new file mode 100644
index 0000000000..f63ec8d25f
--- /dev/null
+++ b/tests/integration/test_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_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 == "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_device_id_in_state.py b/tests/integration/test_device_id_in_state.py
new file mode 100644
index 0000000000..3c5181595f
--- /dev/null
+++ b/tests/integration/test_device_id_in_state.py
@@ -0,0 +1,161 @@
+"""Integration test for device_id in entity state responses."""
+
+from __future__ import annotations
+
+import asyncio
+
+from aioesphomeapi import EntityState
+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:
+            if hasattr(entity, "name") and hasattr(entity, "key"):
+                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 hasattr(s, "state")
+                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 hasattr(s, "state") and isinstance(s.state, bool)
+            ),
+            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 hasattr(s, "state") and isinstance(s.state, str)
+            ),
+            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_host_mode_empty_string_options.py b/tests/integration/test_host_mode_empty_string_options.py
new file mode 100644
index 0000000000..16399dcfb8
--- /dev/null
+++ b/tests/integration/test_host_mode_empty_string_options.py
@@ -0,0 +1,114 @@
+"""Integration test for protobuf encoding of empty string options in select entities."""
+
+from __future__ import annotations
+
+import asyncio
+
+from aioesphomeapi import EntityState, SelectInfo
+import pytest
+
+from .types import APIClientConnectedFactory, RunCompiledFunction
+
+
+@pytest.mark.asyncio
+async def test_host_mode_empty_string_options(
+    yaml_config: str,
+    run_compiled: RunCompiledFunction,
+    api_client_connected: APIClientConnectedFactory,
+) -> None:
+    """Test that select entities with empty string options are correctly encoded in protobuf messages.
+
+    This tests the fix for the bug where the force parameter was not passed in encode_string,
+    causing empty strings in repeated fields to be skipped during encoding but included in
+    size calculation, leading to protobuf decoding errors.
+    """
+    # 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:
+        # Verify we can get device info
+        device_info = await client.device_info()
+        assert device_info is not None
+        assert device_info.name == "host-empty-string-test"
+
+        # Get list of entities - this will encode ListEntitiesSelectResponse messages
+        # with empty string options that would trigger the bug
+        entity_info, services = await client.list_entities_services()
+
+        # Find our select entities
+        select_entities = [e for e in entity_info if isinstance(e, SelectInfo)]
+        assert len(select_entities) == 3, (
+            f"Expected 3 select entities, got {len(select_entities)}"
+        )
+
+        # Verify each select entity by name and check their options
+        selects_by_name = {e.name: e for e in select_entities}
+
+        # Check "Select Empty First" - empty string at beginning
+        assert "Select Empty First" in selects_by_name
+        empty_first = selects_by_name["Select Empty First"]
+        assert len(empty_first.options) == 4
+        assert empty_first.options[0] == ""  # Empty string at beginning
+        assert empty_first.options[1] == "Option A"
+        assert empty_first.options[2] == "Option B"
+        assert empty_first.options[3] == "Option C"
+
+        # Check "Select Empty Middle" - empty string in middle
+        assert "Select Empty Middle" in selects_by_name
+        empty_middle = selects_by_name["Select Empty Middle"]
+        assert len(empty_middle.options) == 5
+        assert empty_middle.options[0] == "Option 1"
+        assert empty_middle.options[1] == "Option 2"
+        assert empty_middle.options[2] == ""  # Empty string in middle
+        assert empty_middle.options[3] == "Option 3"
+        assert empty_middle.options[4] == "Option 4"
+
+        # Check "Select Empty Last" - empty string at end
+        assert "Select Empty Last" in selects_by_name
+        empty_last = selects_by_name["Select Empty Last"]
+        assert len(empty_last.options) == 4
+        assert empty_last.options[0] == "Choice X"
+        assert empty_last.options[1] == "Choice Y"
+        assert empty_last.options[2] == "Choice Z"
+        assert empty_last.options[3] == ""  # Empty string at end
+
+        # 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 receive state updates for select entities
+        # This ensures empty strings work properly in state messages too
+        states: dict[int, EntityState] = {}
+        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
+            # 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)
+
+        # Wait for initial states with timeout
+        try:
+            await asyncio.wait_for(states_received_future, timeout=5.0)
+        except asyncio.TimeoutError:
+            pytest.fail(
+                f"Did not receive states for all select entities. "
+                f"Expected keys: {expected_select_keys}, Received: {received_select_keys}"
+            )
+
+        # Verify we received states for all select entities
+        assert empty_first.key in states
+        assert empty_middle.key in states
+        assert empty_last.key in states
+
+        # 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
new file mode 100644
index 0000000000..cf3fa6916a
--- /dev/null
+++ b/tests/integration/test_host_mode_entity_fields.py
@@ -0,0 +1,93 @@
+"""Integration test for entity bit-packed fields."""
+
+from __future__ import annotations
+
+import asyncio
+
+from aioesphomeapi import EntityCategory, EntityState
+import pytest
+
+from .types import APIClientConnectedFactory, RunCompiledFunction
+
+
+@pytest.mark.asyncio
+async def test_host_mode_entity_fields(
+    yaml_config: str,
+    run_compiled: RunCompiledFunction,
+    api_client_connected: APIClientConnectedFactory,
+) -> None:
+    """Test entity bit-packed fields work correctly with all possible values."""
+    # 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 = {}
+        for entity in entities[0]:
+            if hasattr(entity, "name"):
+                entity_map[entity.name] = entity
+
+        # Test entities that should be visible via API (non-internal)
+        visible_test_cases = [
+            # (entity_name, expected_disabled_by_default, expected_entity_category)
+            ("Test Normal Sensor", False, EntityCategory.NONE),
+            ("Test Disabled Sensor", True, EntityCategory.NONE),
+            ("Test Diagnostic Sensor", False, EntityCategory.DIAGNOSTIC),
+            ("Test Switch", True, EntityCategory.CONFIG),
+            ("Test Binary Sensor", False, EntityCategory.CONFIG),
+            ("Test Number", False, EntityCategory.DIAGNOSTIC),
+        ]
+
+        # Test entities that should NOT be visible via API (internal)
+        internal_entities = [
+            "Test Internal Sensor",
+            "Test Mixed Flags Sensor",
+            "Test All Flags Sensor",
+            "Test Select",
+        ]
+
+        # Verify visible entities
+        for entity_name, expected_disabled, expected_category in visible_test_cases:
+            assert entity_name in entity_map, (
+                f"Entity '{entity_name}' not found - it should be visible via API"
+            )
+            entity = entity_map[entity_name]
+
+            # Check disabled_by_default flag
+            assert entity.disabled_by_default == expected_disabled, (
+                f"{entity_name}: disabled_by_default flag mismatch - "
+                f"expected {expected_disabled}, got {entity.disabled_by_default}"
+            )
+
+            # Check entity_category
+            assert entity.entity_category == expected_category, (
+                f"{entity_name}: entity_category mismatch - "
+                f"expected {expected_category}, got {entity.entity_category}"
+            )
+
+        # Verify internal entities are NOT visible
+        for entity_name in internal_entities:
+            assert entity_name not in entity_map, (
+                f"Entity '{entity_name}' found in API response - "
+                f"internal entities should not be exposed via API"
+            )
+
+        # Subscribe to states to verify has_state flag works
+        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 at least one state
+        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 (which means has_state flag is working)
+        assert len(states) > 0, "No states received - has_state flag may not be working"
diff --git a/tests/integration/test_host_mode_fan_preset.py b/tests/integration/test_host_mode_fan_preset.py
new file mode 100644
index 0000000000..d18b9f08ad
--- /dev/null
+++ b/tests/integration/test_host_mode_fan_preset.py
@@ -0,0 +1,160 @@
+"""Integration test for fan preset mode behavior."""
+
+from __future__ import annotations
+
+import asyncio
+
+from aioesphomeapi import FanInfo, FanState
+import pytest
+
+from .types import APIClientConnectedFactory, RunCompiledFunction
+
+
+@pytest.mark.asyncio
+async def test_host_mode_fan_preset(
+    yaml_config: str,
+    run_compiled: RunCompiledFunction,
+    api_client_connected: APIClientConnectedFactory,
+) -> None:
+    """Test fan preset mode behavior according to Home Assistant guidelines."""
+    # Write, compile and run the ESPHome device, then connect to API
+    async with run_compiled(yaml_config), api_client_connected() as client:
+        # Get all fan entities
+        entities = await client.list_entities_services()
+        fans: list[FanInfo] = []
+        for entity_list in entities:
+            for entity in entity_list:
+                if isinstance(entity, FanInfo):
+                    fans.append(entity)
+
+        # Create a map of fan names to entity info
+        fan_map = {fan.name: fan for fan in fans}
+
+        # Verify we have our test fans
+        assert "Test Fan with Presets" in fan_map
+        assert "Test Fan Simple" in fan_map
+        assert "Test Fan No Speed" in fan_map
+
+        # Get fan with presets
+        fan_presets = fan_map["Test Fan with Presets"]
+        assert fan_presets.supports_speed is True
+        assert fan_presets.supported_speed_count == 5
+        assert fan_presets.supports_oscillation is True
+        assert fan_presets.supports_direction is True
+        assert set(fan_presets.supported_preset_modes) == {"Eco", "Sleep", "Turbo"}
+
+        # 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(
+            key=fan_presets.key,
+            state=True,
+        )
+        await asyncio.wait_for(state_event.wait(), timeout=2.0)
+
+        fan_state = states[fan_presets.key]
+        assert fan_state.state is True
+        assert fan_state.speed_level == 5  # Should be max speed (100%)
+        assert fan_state.preset_mode == ""
+
+        # Turn off
+        state_event.clear()
+        client.fan_command(
+            key=fan_presets.key,
+            state=False,
+        )
+        await asyncio.wait_for(state_event.wait(), timeout=2.0)
+
+        # Test 2: Turn on fan with preset mode - should NOT set speed to 100%
+        state_event.clear()
+        client.fan_command(
+            key=fan_presets.key,
+            state=True,
+            preset_mode="Eco",
+        )
+        await asyncio.wait_for(state_event.wait(), timeout=2.0)
+
+        fan_state = states[fan_presets.key]
+        assert fan_state.state is True
+        assert fan_state.preset_mode == "Eco"
+        # Speed should be whatever the preset sets, not forced to 100%
+
+        # Test 3: Setting speed should clear preset mode
+        state_event.clear()
+        client.fan_command(
+            key=fan_presets.key,
+            speed_level=3,
+        )
+        await asyncio.wait_for(state_event.wait(), timeout=2.0)
+
+        fan_state = states[fan_presets.key]
+        assert fan_state.state is True
+        assert fan_state.speed_level == 3
+        assert fan_state.preset_mode == ""  # Preset mode should be cleared
+
+        # Test 4: Setting preset mode should work when fan is already on
+        state_event.clear()
+        client.fan_command(
+            key=fan_presets.key,
+            preset_mode="Sleep",
+        )
+        await asyncio.wait_for(state_event.wait(), timeout=2.0)
+
+        fan_state = states[fan_presets.key]
+        assert fan_state.state is True
+        assert fan_state.preset_mode == "Sleep"
+
+        # Turn off
+        state_event.clear()
+        client.fan_command(
+            key=fan_presets.key,
+            state=False,
+        )
+        await asyncio.wait_for(state_event.wait(), timeout=2.0)
+
+        # Test 5: Turn on fan with specific speed
+        state_event.clear()
+        client.fan_command(
+            key=fan_presets.key,
+            state=True,
+            speed_level=2,
+        )
+        await asyncio.wait_for(state_event.wait(), timeout=2.0)
+
+        fan_state = states[fan_presets.key]
+        assert fan_state.state is True
+        assert fan_state.speed_level == 2
+        assert fan_state.preset_mode == ""
+
+        # Test 6: Test fan with no speed support
+        fan_no_speed = fan_map["Test Fan No Speed"]
+        assert fan_no_speed.supports_speed is False
+
+        state_event.clear()
+        client.fan_command(
+            key=fan_no_speed.key,
+            state=True,
+        )
+        await asyncio.wait_for(state_event.wait(), timeout=2.0)
+
+        fan_state = states[fan_no_speed.key]
+        assert fan_state.state is True
+        # No speed should be set for fans that don't support speed
diff --git a/tests/integration/test_host_mode_many_entities.py b/tests/integration/test_host_mode_many_entities.py
index d5622e6fa4..005728b8c6 100644
--- a/tests/integration/test_host_mode_many_entities.py
+++ b/tests/integration/test_host_mode_many_entities.py
@@ -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 hasattr(s, "state") 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 hasattr(s, "state") 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)
         ]
 
+        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..049f7db619 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
 
@@ -47,3 +48,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_large_message_batching.py b/tests/integration/test_large_message_batching.py
new file mode 100644
index 0000000000..399fd39dd3
--- /dev/null
+++ b/tests/integration/test_large_message_batching.py
@@ -0,0 +1,59 @@
+"""Integration test for API handling of large messages exceeding batch size."""
+
+from __future__ import annotations
+
+from aioesphomeapi import SelectInfo
+import pytest
+
+from .types import APIClientConnectedFactory, RunCompiledFunction
+
+
+@pytest.mark.asyncio
+async def test_large_message_batching(
+    yaml_config: str,
+    run_compiled: RunCompiledFunction,
+    api_client_connected: APIClientConnectedFactory,
+) -> None:
+    """Test API can handle large messages (>1390 bytes) in batches."""
+    # Write, compile and run the ESPHome device, then connect to API
+    async with run_compiled(yaml_config), api_client_connected() as client:
+        # Verify we can get device info
+        device_info = await client.device_info()
+        assert device_info is not None
+        assert device_info.name == "large-message-test"
+
+        # List entities - this will include our select with many options
+        entity_info, services = await client.list_entities_services()
+
+        # Find our large select entity
+        large_select = None
+        for entity in entity_info:
+            if isinstance(entity, SelectInfo) and entity.object_id == "large_select":
+                large_select = entity
+                break
+
+        assert large_select is not None, "Could not find large_select entity"
+
+        # Verify the select has all its options
+        # We created 100 options with long names
+        assert len(large_select.options) == 100, (
+            f"Expected 100 options, got {len(large_select.options)}"
+        )
+
+        # Verify all options are present and correct
+        for i in range(100):
+            expected_option = f"Option {i:03d} - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
+            assert expected_option in large_select.options, (
+                f"Missing option: {expected_option}"
+            )
+
+        # Also verify we can still receive other entities in the same batch
+        # Count total entities - should have at least our select plus some sensors
+        entity_count = len(entity_info)
+        assert entity_count >= 4, f"Expected at least 4 entities, got {entity_count}"
+
+        # Verify we have different entity types (not just selects)
+        entity_types = {type(entity).__name__ for entity in entity_info}
+        assert len(entity_types) >= 2, (
+            f"Expected multiple entity types, got {entity_types}"
+        )
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_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_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_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_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
 ):