diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 40f520a0575..25ebc934586 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -601,7 +601,7 @@ async def to_code(config: ConfigType) -> None: if (encryption_config := config.get(CONF_ENCRYPTION, None)) is not None: if key := encryption_config.get(CONF_KEY): - cg.add(var.set_noise_psk(new_psk_progmem(config[CONF_ID], key))) + cg.add(var.set_noise_psk(new_psk_progmem(key))) cg.add_define("USE_API_NOISE_PSK_FROM_YAML") else: # No key provided, but encryption desired diff --git a/esphome/components/es8311/es8311.cpp b/esphome/components/es8311/es8311.cpp index 0386d84200f..9a4da3744a1 100644 --- a/esphome/components/es8311/es8311.cpp +++ b/esphome/components/es8311/es8311.cpp @@ -92,6 +92,21 @@ uint8_t ES8311::calculate_resolution_value(ES8311Resolution resolution) { } } +/// Encodes the MCLK pre-multiplier for REG02 bits 3-4. The coefficient table stores the multiplier as the factor +/// itself (1, 2, 4 or 8), while the register takes 0, 1, 2 or 3 (ES8311 datasheet, register 0x02 MULT_PRE). +static uint8_t encode_pre_mult(uint8_t pre_mult) { + switch (pre_mult) { + case 2: + return 1; + case 4: + return 2; + case 8: + return 3; + default: + return 0; + } +} + const ES8311Coefficient *ES8311::get_coefficient(uint32_t mclk, uint32_t rate) { for (const auto &coefficient : ES8311_COEFFICIENTS) { if (coefficient.mclk == mclk && coefficient.rate == rate) @@ -127,7 +142,7 @@ bool ES8311::configure_clock_() { ES8311_ERROR_CHECK(this->read_byte(ES8311_REG02_CLK_MANAGER, ®02)); reg02 &= 0x07; reg02 |= (coefficient->pre_div - 1) << 5; - reg02 |= coefficient->pre_mult << 3; + reg02 |= encode_pre_mult(coefficient->pre_mult) << 3; ES8311_ERROR_CHECK(this->write_byte(ES8311_REG02_CLK_MANAGER, reg02)); // Register 0x03 diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index fd4fb15ff69..950ae6e0bc1 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -235,14 +235,14 @@ class ESP32BLE final : public Component { #ifdef USE_ESP32_BLE_ADVERTISING BLEAdvertising *advertising_{}; // 4 bytes (pointer) #endif - const char *name_{nullptr}; // 4 bytes (pointer to string literal in flash) - esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; // 4 bytes (enum) - uint32_t advertising_cycle_time_{}; // 4 bytes + const char *name_{nullptr}; // 4 bytes (pointer to string literal in flash) + 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) + esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; // 1 byte (uint8_t typedef) BLEComponentState state_{BLE_COMPONENT_STATE_OFF}; // 1 byte (uint8_t enum) bool enable_on_boot_{}; // 1 byte #ifdef USE_ESP32_BLE_ADVERTISING diff --git a/esphome/components/esphome/ota/__init__.py b/esphome/components/esphome/ota/__init__.py index 6ecae24e299..ffd7c509497 100644 --- a/esphome/components/esphome/ota/__init__.py +++ b/esphome/components/esphome/ota/__init__.py @@ -360,7 +360,7 @@ async def to_code(config: ConfigType) -> None: # Build time key: the ota keeps its own pointer so safe mode, which # has no api server, still has it cg.add_define("USE_OTA_ENCRYPTION") - cg.add(var.set_noise_psk(new_psk_progmem(config[CONF_ID], key))) + cg.add(var.set_noise_psk(new_psk_progmem(key))) elif CONF_ENCRYPTION in api_conf: # Runtime key: found in the api server, or in preferences in safe mode cg.add_define("USE_OTA_ENCRYPTION") diff --git a/esphome/components/homeassistant/number/__init__.py b/esphome/components/homeassistant/number/__init__.py index ab1389e13a3..9658d3dc84e 100644 --- a/esphome/components/homeassistant/number/__init__.py +++ b/esphome/components/homeassistant/number/__init__.py @@ -7,19 +7,23 @@ from .. import ( HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA, homeassistant_ns, setup_home_assistant_entity, + validate_entity_domain, ) CODEOWNERS = ["@landonr"] DEPENDENCIES = ["api"] +SUPPORTED_DOMAINS = ["input_number", "number"] + HomeassistantNumber = homeassistant_ns.class_( "HomeassistantNumber", number.Number, cg.Component ) -CONFIG_SCHEMA = ( +CONFIG_SCHEMA = cv.All( number.number_schema(HomeassistantNumber) .extend(HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA) - .extend(cv.COMPONENT_SCHEMA) + .extend(cv.COMPONENT_SCHEMA), + validate_entity_domain("number", SUPPORTED_DOMAINS), ) diff --git a/esphome/components/homeassistant/number/homeassistant_number.cpp b/esphome/components/homeassistant/number/homeassistant_number.cpp index 965f91d202b..553c2466f84 100644 --- a/esphome/components/homeassistant/number/homeassistant_number.cpp +++ b/esphome/components/homeassistant/number/homeassistant_number.cpp @@ -1,5 +1,7 @@ #include "homeassistant_number.h" +#include + #include "esphome/components/api/api_pb2.h" #include "esphome/components/api/api_server.h" #include "esphome/core/log.h" @@ -80,12 +82,19 @@ void HomeassistantNumber::control(float value) { this->publish_state(value); - static constexpr auto SERVICE_NAME = StringRef::from_lit("number.set_value"); + static constexpr auto NUMBER_SERVICE_NAME = StringRef::from_lit("number.set_value"); + static constexpr auto INPUT_NUMBER_SERVICE_NAME = StringRef::from_lit("input_number.set_value"); + static constexpr char INPUT_PREFIX[] = "input_"; static constexpr auto ENTITY_ID_KEY = StringRef::from_lit("entity_id"); static constexpr auto VALUE_KEY = StringRef::from_lit("value"); api::HomeassistantActionRequest resp; - resp.service = SERVICE_NAME; + // input_number entities have their own set_value action + if (strncmp(this->entity_id_, INPUT_PREFIX, sizeof(INPUT_PREFIX) - 1) == 0) { + resp.service = INPUT_NUMBER_SERVICE_NAME; + } else { + resp.service = NUMBER_SERVICE_NAME; + } resp.data.init(2); auto &entity_id = resp.data.emplace_back(); diff --git a/esphome/components/http_request/http_request.cpp b/esphome/components/http_request/http_request.cpp index d45208ed5df..37e0de5d6a0 100644 --- a/esphome/components/http_request/http_request.cpp +++ b/esphome/components/http_request/http_request.cpp @@ -25,11 +25,10 @@ std::string HttpContainer::get_response_header(const std::string &header_name) { auto lower = str_lower_case(header_name); // NOLINT for (const auto &entry : this->response_headers_) { if (entry.name == lower) { - ESP_LOGD(TAG, "Header with name %s found with value %s", lower.c_str(), entry.value.c_str()); + ESP_LOGV(TAG, "Header with name %s found with value %s", lower.c_str(), entry.value.c_str()); return entry.value; } } - ESP_LOGW(TAG, "No header with name %s found", lower.c_str()); return ""; } diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index 71668b8556f..68696118ea8 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -268,6 +268,11 @@ class HttpContainer : public Parented { return !this->is_chunked_ && this->bytes_read_ >= this->content_length; } + /** + * @brief Get the value of a response header by name. + * @param header_name The name of the header to retrieve. + * @return The value of the header if found, otherwise an empty string. + */ std::string get_response_header(const std::string &header_name); protected: diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h index 37895ac4e79..1b17380ed87 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h @@ -60,11 +60,11 @@ class I2SAudioMicrophone final : public I2SAudioIn, public microphone::Microphon gpio_num_t din_pin_{I2S_GPIO_UNUSED}; i2s_chan_handle_t rx_handle_; - bool pdm_{false}; #if SOC_I2S_SUPPORTS_PDM_RX i2s_pdm_dsr_t pdm_dsr_{I2S_PDM_DSR_8S}; #endif + bool pdm_{false}; bool correct_dc_offset_; bool locked_driver_{false}; int32_t dc_offset_prev_input_{0}; diff --git a/esphome/components/improv_ble/improv_ble_component.h b/esphome/components/improv_ble/improv_ble_component.h index 2552bed69b5..126663ed000 100644 --- a/esphome/components/improv_ble/improv_ble_component.h +++ b/esphome/components/improv_ble/improv_ble_component.h @@ -72,9 +72,6 @@ class ImprovBLEComponent final : public Component, public improv_base::ImprovBas improv::Error get_improv_error_state() const { return this->error_state_; } protected: - bool should_start_{false}; - bool setup_complete_{false}; - uint32_t identify_start_{0}; uint32_t identify_duration_; uint32_t authorized_start_{0}; @@ -84,6 +81,14 @@ class ImprovBLEComponent final : public Component, public improv_base::ImprovBas std::vector incoming_data_; wifi::WiFiAP connecting_sta_; + // Single byte fields here fill the alignment gap between connecting_sta_ and the next pointer + improv::State state_{improv::STATE_STOPPED}; + improv::Error error_state_{improv::ERROR_NONE}; + bool should_start_{false}; + bool setup_complete_{false}; + bool status_indicator_state_{false}; + bool advertising_device_name_{false}; + bool advertising_requested_{false}; BLEService *service_{nullptr}; BLECharacteristic *status_{nullptr}; @@ -99,16 +104,11 @@ class ImprovBLEComponent final : public Component, public improv_base::ImprovBas output::BinaryOutput *status_indicator_{nullptr}; #endif - improv::State state_{improv::STATE_STOPPED}; - improv::Error error_state_{improv::ERROR_NONE}; #ifdef USE_IMPROV_BLE_STATE_CALLBACK CallbackManager state_callback_{}; #endif - bool status_indicator_state_{false}; uint32_t last_name_adv_time_{0}; - bool advertising_device_name_{false}; - bool advertising_requested_{false}; void set_status_indicator_state_(bool state); void update_advertising_type_(); void request_advertising_(); diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h index c4f06ad2248..d6285cce8af 100644 --- a/esphome/components/ld2450/ld2450.h +++ b/esphome/components/ld2450/ld2450.h @@ -176,6 +176,10 @@ class LD2450Component : public Component, public uart::UARTDevice { bool bluetooth_on_{false}; Target target_info_[MAX_TARGETS]; Zone zone_config_[MAX_ZONES]; +#ifdef USE_TEXT_SENSOR + // Byte aligned, so it packs after zone_config_ instead of padding before data_callback_ + std::array, MAX_TARGETS> direction_dedup_{}; +#endif #ifdef USE_NUMBER ESPPreferenceObject pref_; // only used when numbers are in use @@ -194,7 +198,6 @@ class LD2450Component : public Component, public uart::UARTDevice { #endif #ifdef USE_TEXT_SENSOR std::array direction_text_sensors_{}; - std::array, MAX_TARGETS> direction_dedup_{}; #endif LazyCallbackManager data_callback_; diff --git a/esphome/components/ledc/ledc_output.h b/esphome/components/ledc/ledc_output.h index de63325455b..1df0f675729 100644 --- a/esphome/components/ledc/ledc_output.h +++ b/esphome/components/ledc/ledc_output.h @@ -35,11 +35,11 @@ class LEDCOutput final : public output::FloatOutput, public Component { InternalGPIOPin *pin_; uint8_t channel_{}; uint8_t bit_depth_{}; + bool initialized_ = false; float phase_angle_{0.0f}; float frequency_{}; float duty_{0.0f}; uint32_t last_duty_{UINT32_MAX}; - bool initialized_ = false; }; } // namespace esphome::ledc diff --git a/esphome/components/microphone/microphone_source.h b/esphome/components/microphone/microphone_source.h index d7a33524322..770399fef59 100644 --- a/esphome/components/microphone/microphone_source.h +++ b/esphome/components/microphone/microphone_source.h @@ -34,7 +34,7 @@ class MicrophoneSource final { */ public: MicrophoneSource(Microphone *mic, uint8_t bits_per_sample, int32_t gain_factor, bool passive) - : mic_(mic), bits_per_sample_(bits_per_sample), gain_factor_(gain_factor), passive_(passive) {} + : mic_(mic), gain_factor_(gain_factor), passive_(passive), bits_per_sample_(bits_per_sample) {} /// @brief Enables a channel to be processed through the callback. /// @@ -81,11 +81,11 @@ class MicrophoneSource final { std::shared_ptr> processed_samples_; Microphone *mic_; - uint8_t bits_per_sample_; std::bitset<8> channels_; int32_t gain_factor_; bool enabled_{false}; bool passive_; // Only pass audio if ``mic_`` is already running + uint8_t bits_per_sample_; }; } // namespace esphome::microphone diff --git a/esphome/components/mixer/speaker/mixer_speaker.h b/esphome/components/mixer/speaker/mixer_speaker.h index 494443d6951..04b0cb94f40 100644 --- a/esphome/components/mixer/speaker/mixer_speaker.h +++ b/esphome/components/mixer/speaker/mixer_speaker.h @@ -107,12 +107,12 @@ class SourceSpeaker final : public speaker::Speaker, public Component { bool stop_gracefully_{false}; bool pause_state_{false}; + std::atomic has_contributed_{false}; // Tracks if source has contributed during this session esp_audio_libs::gain::GainRamp ducking_ramp_; std::atomic pending_playback_frames_{0}; std::atomic playback_delay_frames_{0}; // Frames in output pipeline when this source started contributing - std::atomic has_contributed_{false}; // Tracks if source has contributed during this session EventGroupHandle_t event_group_{nullptr}; uint32_t stopping_start_ms_{0}; diff --git a/esphome/components/noise/__init__.py b/esphome/components/noise/__init__.py index 19ad36612de..d9880fe6f9c 100644 --- a/esphome/components/noise/__init__.py +++ b/esphome/components/noise/__init__.py @@ -5,12 +5,11 @@ from typing import Any import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_ENCRYPTION, CONF_KEY -from esphome.core import CORE, ID +from esphome.core import CORE from esphome.cpp_generator import MockObj from esphome.types import ConfigType CODEOWNERS = ["@esphome/core"] -DOMAIN = "noise" # Keep in sync with platformio.ini and esphome/idf_component.yml. # LIBSODIUM_VERSION must match the version noise-c pins in its manifests. @@ -74,18 +73,13 @@ def static_encryption_key(conf: ConfigType) -> str | None: return (conf.get(CONF_ENCRYPTION) or {}).get(CONF_KEY) or None -def new_psk_progmem(parent_id: ID, key: str) -> MockObj: +def new_psk_progmem(key: str) -> MockObj: """Emit the decoded key as a PROGMEM array; the component keeps a pointer so the key never occupies RAM. Components sharing one key (api and ota) share the array.""" - decoded = decode_encryption_key(key) - arrays: dict[bytes, MockObj] = CORE.data.setdefault(DOMAIN, {}) - if (array := arrays.get(decoded)) is None: - array = arrays[decoded] = cg.progmem_array( - ID(f"{parent_id.id}_psk", is_declaration=True, type=cg.uint8), - list(decoded), - ) - return array + return cg.shared_progmem_array( + "noise_psk", cg.uint8, list(decode_encryption_key(key)) + ) def encryption_schema(config: ConfigType | None) -> ConfigType: diff --git a/esphome/components/remote_receiver/remote_receiver.h b/esphome/components/remote_receiver/remote_receiver.h index 6f93979b183..1c1c473f478 100644 --- a/esphome/components/remote_receiver/remote_receiver.h +++ b/esphome/components/remote_receiver/remote_receiver.h @@ -88,8 +88,8 @@ class RemoteReceiverComponent final : public remote_base::RemoteReceiverBase, rmt_channel_handle_t channel_{NULL}; uint32_t filter_symbols_{0}; uint32_t receive_symbols_{0}; - bool with_dma_{false}; uint32_t carrier_frequency_{0}; + bool with_dma_{false}; uint8_t carrier_duty_percent_{100}; #endif diff --git a/esphome/components/ring_buffer/ring_buffer.cpp b/esphome/components/ring_buffer/ring_buffer.cpp index 9604290cf03..dfcc53ebb2e 100644 --- a/esphome/components/ring_buffer/ring_buffer.cpp +++ b/esphome/components/ring_buffer/ring_buffer.cpp @@ -10,8 +10,9 @@ namespace esphome::ring_buffer { static const char *const TAG = "ring_buffer"; RingBuffer::~RingBuffer() { - if (this->handle_ != nullptr) { - vRingbufferDelete(this->handle_); + // create() only builds the ring buffer once storage_ is allocated + if (this->storage_ != nullptr) { + vRingbufferDelete(this->handle_()); RAMAllocator allocator; allocator.deallocate(this->storage_, this->size_); } @@ -31,7 +32,12 @@ std::unique_ptr RingBuffer::create(size_t len, MemoryPreference pref return nullptr; } - rb->handle_ = xRingbufferCreateStatic(rb->size_, RINGBUF_TYPE_BYTEBUF, rb->storage_, &rb->structure_); + // handle_() assumes the returned handle is &structure_; NULL means the arguments were rejected + if (xRingbufferCreateStatic(rb->size_, RINGBUF_TYPE_BYTEBUF, rb->storage_, &rb->structure_) != rb->handle_()) { + allocator.deallocate(rb->storage_, rb->size_); + rb->storage_ = nullptr; + return nullptr; + } ESP_LOGD(TAG, "Created ring buffer with size %u", len); return rb; @@ -39,16 +45,16 @@ std::unique_ptr RingBuffer::create(size_t len, MemoryPreference pref void *RingBuffer::receive_acquire(size_t &length, size_t max_length, TickType_t ticks_to_wait) { length = 0; - void *buffer_data = xRingbufferReceiveUpTo(this->handle_, &length, ticks_to_wait, max_length); + void *buffer_data = xRingbufferReceiveUpTo(this->handle_(), &length, ticks_to_wait, max_length); return buffer_data; } -void RingBuffer::receive_release(void *item) { vRingbufferReturnItem(this->handle_, item); } +void RingBuffer::receive_release(void *item) { vRingbufferReturnItem(this->handle_(), item); } size_t RingBuffer::read(void *data, size_t len, TickType_t ticks_to_wait) { size_t bytes_read = 0; - void *buffer_data = xRingbufferReceiveUpTo(this->handle_, &bytes_read, ticks_to_wait, len); + void *buffer_data = xRingbufferReceiveUpTo(this->handle_(), &bytes_read, ticks_to_wait, len); if (buffer_data == nullptr) { return 0; @@ -56,14 +62,14 @@ size_t RingBuffer::read(void *data, size_t len, TickType_t ticks_to_wait) { std::memcpy(data, buffer_data, bytes_read); - vRingbufferReturnItem(this->handle_, buffer_data); + vRingbufferReturnItem(this->handle_(), buffer_data); if (bytes_read < len) { // Data may have wrapped around, so read a second time to receive the remainder size_t follow_up_bytes_read = 0; size_t bytes_remaining = len - bytes_read; - buffer_data = xRingbufferReceiveUpTo(this->handle_, &follow_up_bytes_read, 0, bytes_remaining); + buffer_data = xRingbufferReceiveUpTo(this->handle_(), &follow_up_bytes_read, 0, bytes_remaining); if (buffer_data == nullptr) { return bytes_read; @@ -71,7 +77,7 @@ size_t RingBuffer::read(void *data, size_t len, TickType_t ticks_to_wait) { std::memcpy((void *) ((uint8_t *) (data) + bytes_read), buffer_data, follow_up_bytes_read); - vRingbufferReturnItem(this->handle_, buffer_data); + vRingbufferReturnItem(this->handle_(), buffer_data); bytes_read += follow_up_bytes_read; } @@ -89,13 +95,13 @@ size_t RingBuffer::write(const void *data, size_t len) { size_t RingBuffer::write_without_replacement(const void *data, size_t len, TickType_t ticks_to_wait, bool write_partial) { - if (!xRingbufferSend(this->handle_, data, len, ticks_to_wait)) { + if (!xRingbufferSend(this->handle_(), data, len, ticks_to_wait)) { if (!write_partial) { return 0; // Not enough space available and not allowed to write partial data } // Couldn't fit all the data, write what will fit size_t free = std::min(this->free(), len); - if (xRingbufferSend(this->handle_, data, free, 0)) { + if (xRingbufferSend(this->handle_(), data, free, 0)) { return free; } return 0; @@ -105,11 +111,11 @@ size_t RingBuffer::write_without_replacement(const void *data, size_t len, TickT size_t RingBuffer::available() const { UBaseType_t ux_items_waiting = 0; - vRingbufferGetInfo(this->handle_, nullptr, nullptr, nullptr, nullptr, &ux_items_waiting); + vRingbufferGetInfo(this->handle_(), nullptr, nullptr, nullptr, nullptr, &ux_items_waiting); return ux_items_waiting; } -size_t RingBuffer::free() const { return xRingbufferGetCurFreeSize(this->handle_); } +size_t RingBuffer::free() const { return xRingbufferGetCurFreeSize(this->handle_()); } BaseType_t RingBuffer::reset() { // Discards all the available data @@ -119,15 +125,15 @@ BaseType_t RingBuffer::reset() { bool RingBuffer::discard_bytes_(size_t discard_bytes) { size_t bytes_read = 0; - void *buffer_data = xRingbufferReceiveUpTo(this->handle_, &bytes_read, 0, discard_bytes); + void *buffer_data = xRingbufferReceiveUpTo(this->handle_(), &bytes_read, 0, discard_bytes); if (buffer_data != nullptr) - vRingbufferReturnItem(this->handle_, buffer_data); + vRingbufferReturnItem(this->handle_(), buffer_data); if (bytes_read < discard_bytes) { size_t wrapped_bytes_read = 0; - buffer_data = xRingbufferReceiveUpTo(this->handle_, &wrapped_bytes_read, 0, discard_bytes - bytes_read); + buffer_data = xRingbufferReceiveUpTo(this->handle_(), &wrapped_bytes_read, 0, discard_bytes - bytes_read); if (buffer_data != nullptr) { - vRingbufferReturnItem(this->handle_, buffer_data); + vRingbufferReturnItem(this->handle_(), buffer_data); bytes_read += wrapped_bytes_read; } } diff --git a/esphome/components/ring_buffer/ring_buffer.h b/esphome/components/ring_buffer/ring_buffer.h index 62094899d78..ed2d4b43456 100644 --- a/esphome/components/ring_buffer/ring_buffer.h +++ b/esphome/components/ring_buffer/ring_buffer.h @@ -115,7 +115,9 @@ class RingBuffer { /// @return True if all bytes were successfully discarded, false otherwise bool discard_bytes_(size_t discard_bytes); - RingbufHandle_t handle_{nullptr}; + // xRingbufferCreateStatic() returns the static structure itself as the handle + RingbufHandle_t handle_() const { return const_cast(&this->structure_); } + StaticRingbuffer_t structure_; uint8_t *storage_{nullptr}; size_t size_{0}; diff --git a/esphome/components/rtttl/rtttl.h b/esphome/components/rtttl/rtttl.h index 7a32b79b177..055204571e1 100644 --- a/esphome/components/rtttl/rtttl.h +++ b/esphome/components/rtttl/rtttl.h @@ -82,14 +82,14 @@ class Rtttl final : public Component { uint16_t note_duration_{0}; /// The duration of a whole note in milliseconds. uint16_t wholenote_duration_; + /// The current state of the RTTTL player. + State state_{State::STOPPED}; /// The time in milliseconds since microcontroller boot when the last note was started. uint32_t last_note_start_time_; /// The frequency of the current note in Hz. uint32_t output_freq_{0}; /// The gain of the output. float gain_{0.6f}; - /// The current state of the RTTTL player. - State state_{State::STOPPED}; #ifdef USE_OUTPUT /// The output to write the sound to. diff --git a/esphome/components/script/script.h b/esphome/components/script/script.h index b9ea75e0bcf..97b27af498d 100644 --- a/esphome/components/script/script.h +++ b/esphome/components/script/script.h @@ -254,7 +254,8 @@ template class ScriptExecuteAction, T } Script *script_; - Args args_; + // Empty tuple for scripts without parameters; takes no space then + [[no_unique_address]] Args args_; }; /** Wait for a script to finish before continuing. diff --git a/esphome/components/sendspin/sendspin_hub.h b/esphome/components/sendspin/sendspin_hub.h index 8ab70d61ef1..ea09dafe689 100644 --- a/esphome/components/sendspin/sendspin_hub.h +++ b/esphome/components/sendspin/sendspin_hub.h @@ -308,6 +308,9 @@ class SendspinHub final : public Component, CallbackManager group_update_callbacks_{}; bool task_stack_in_psram_{false}; +#ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE + bool mdns_advertised_{false}; // Last state requested from mdns +#endif // Requested client state, applied from loop(). Empty until the switch restores its state. std::optional enabled_; @@ -320,7 +323,6 @@ class SendspinHub final : public Component, #ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE mdns::MDNSComponent *mdns_{nullptr}; - bool mdns_advertised_{false}; // Last state requested from mdns #endif }; diff --git a/esphome/components/speaker/speaker.h b/esphome/components/speaker/speaker.h index 01e9ca042e0..277b36a171a 100644 --- a/esphome/components/speaker/speaker.h +++ b/esphome/components/speaker/speaker.h @@ -127,9 +127,9 @@ class Speaker { #endif State state_{STATE_STOPPED}; + bool mute_state_{false}; audio::AudioStreamInfo audio_stream_info_; float volume_{1.0f}; - bool mute_state_{false}; #ifdef USE_AUDIO_DAC audio_dac::AudioDac *audio_dac_{nullptr}; diff --git a/esphome/components/template/number/template_number.h b/esphome/components/template/number/template_number.h index e51e858ccf3..2c4932f7e87 100644 --- a/esphome/components/template/number/template_number.h +++ b/esphome/components/template/number/template_number.h @@ -25,8 +25,8 @@ class TemplateNumber final : public number::Number, public PollingComponent { protected: void control(float value) override; bool optimistic_{false}; - float initial_value_{NAN}; bool restore_value_{false}; + float initial_value_{NAN}; Trigger set_trigger_; TemplateLambda f_; diff --git a/esphome/components/template/select/template_select.h b/esphome/components/template/select/template_select.h index 1cc28a36d37..c6a1b8bd088 100644 --- a/esphome/components/template/select/template_select.h +++ b/esphome/components/template/select/template_select.h @@ -9,7 +9,8 @@ namespace esphome::template_ { -struct Empty {}; +// Distinct per member: two empty members of the same type cannot share an address +template struct Empty {}; class BaseTemplateSelect : public select::Select, public PollingComponent {}; void dump_config_helper(BaseTemplateSelect *sel_comp, bool optimistic, bool has_lambda, size_t initial_option_index, @@ -59,8 +60,8 @@ class TemplateSelect : public BaseTemplateSelect { if constexpr (RESTORE_VALUE) this->pref_.save(&index); } - [[no_unique_address]] std::conditional_t, Empty> f_{}; - [[no_unique_address]] std::conditional_t pref_{}; + [[no_unique_address]] std::conditional_t, Empty<0>> f_{}; + [[no_unique_address]] std::conditional_t> pref_{}; }; /// Template select with set_action trigger - only instantiated when set_action is configured diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index e99ee07099f..ae5eee3e227 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -44,7 +44,7 @@ enum VoiceAssistantFeature : uint32_t { FEATURE_MULTI_CHANNEL_AUDIO = 1 << 6, }; -enum class State { +enum class State : uint8_t { IDLE, START_MICROPHONE, STARTING_MICROPHONE, @@ -96,7 +96,7 @@ struct Configuration { }; #ifdef USE_MEDIA_PLAYER -enum class MediaPlayerResponseState { +enum class MediaPlayerResponseState : uint8_t { IDLE, URL_SENT, PLAYING, @@ -237,6 +237,7 @@ class VoiceAssistant final : public Component { void set_state_(State state); void set_state_(State state, State desired_state); + bool start_udp_socket_(); void signal_stop_(); void start_playback_timeout_(); @@ -280,8 +281,6 @@ class VoiceAssistant final : public Component { Trigger timer_updated_trigger_; Trigger timer_cancelled_trigger_; Trigger &> timer_tick_trigger_; - bool has_timers_{false}; - bool timer_tick_running_{false}; microphone::MicrophoneSource *mic_source_{nullptr}; microphone::MicrophoneSource *mic_source2_{nullptr}; @@ -292,19 +291,12 @@ class VoiceAssistant final : public Component { size_t speaker_buffer_index_{0}; size_t speaker_buffer_size_{0}; size_t speaker_bytes_received_{0}; - bool wait_for_stream_end_{false}; - bool stream_ended_{false}; #endif #ifdef USE_MEDIA_PLAYER media_player::MediaPlayer *media_player_{nullptr}; std::string tts_response_url_; - bool started_streaming_tts_{false}; - - MediaPlayerResponseState media_player_response_state_{MediaPlayerResponseState::IDLE}; #endif - bool local_output_{false}; - std::string conversation_id_; std::string wake_word_; @@ -324,29 +316,37 @@ class VoiceAssistant final : public Component { // prolonged one can be detected and stopped; 0 means no imbalance is currently being timed. uint32_t audio_channel_stall_start_{0}; - bool use_wake_word_; - uint8_t noise_suppression_level_; - uint8_t auto_gain_; float volume_multiplier_; uint32_t conversation_timeout_; - bool continuous_{false}; - bool silence_detection_; - - bool continue_conversation_{false}; - - State state_{State::IDLE}; - State desired_state_{State::IDLE}; - - AudioMode audio_mode_{AUDIO_MODE_UDP}; - bool udp_socket_running_{false}; - bool start_udp_socket_(); - Configuration config_{}; #ifdef USE_MICRO_WAKE_WORD micro_wake_word::MicroWakeWord *micro_wake_word_{nullptr}; #endif + + // 1 byte members grouped at the end so they share padding instead of each taking a word + State state_{State::IDLE}; + State desired_state_{State::IDLE}; + AudioMode audio_mode_{AUDIO_MODE_UDP}; + bool has_timers_{false}; + bool timer_tick_running_{false}; +#ifdef USE_SPEAKER + bool wait_for_stream_end_{false}; + bool stream_ended_{false}; +#endif +#ifdef USE_MEDIA_PLAYER + MediaPlayerResponseState media_player_response_state_{MediaPlayerResponseState::IDLE}; + bool started_streaming_tts_{false}; +#endif + bool local_output_{false}; + bool use_wake_word_; + uint8_t noise_suppression_level_; + uint8_t auto_gain_; + bool continuous_{false}; + bool silence_detection_; + bool continue_conversation_{false}; + bool udp_socket_running_{false}; }; extern VoiceAssistant *global_voice_assistant; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 67913796499..082c059a14a 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -303,7 +303,7 @@ class WiFiAP { bssid_t bssid_{}; // 6 bytes, all zeros = any/not set uint8_t channel_{0}; // 1 byte, 0 = auto/not set int8_t priority_{0}; // 1 byte - bool hidden_{false}; // 1 byte (+ 3 bytes end padding to 4-byte align) + bool hidden_{false}; // 1 byte; WiFiAP is byte aligned unless manual IP or EAP adds a 4 byte aligned member }; class WiFiScanResult { @@ -874,9 +874,6 @@ class WiFiComponent final : public Component { #ifdef WIFI_SCAN_RESULTS_LOCK_ENABLED Mutex scan_result_lock_; #endif -#ifdef USE_WIFI_AP - WiFiAP ap_; -#endif #ifdef USE_WIFI_IP_STATE_LISTENERS StaticVector ip_state_listeners_; #endif @@ -924,6 +921,9 @@ class WiFiComponent final : public Component { uint32_t roaming_scan_end_{0}; // Timestamp when last roaming scan completed #ifdef USE_WIFI_AP uint32_t ap_timeout_{90000}; // Keep in sync with DEFAULT_AP_TIMEOUT in __init__.py + // WiFiAP is byte aligned unless manual IP or EAP is enabled; placed before the + // 1-byte members so they pack into its trailing bytes instead of padding after it + WiFiAP ap_; #endif // 1-byte enums and integers @@ -983,7 +983,8 @@ class WiFiComponent final : public Component { // Bools and bitfields // Pending listener callbacks deferred from platform callbacks to main loop. - struct { + // Empty when no listener needs deferring (e.g. ESP32 without connect state listeners) + [[no_unique_address]] struct { #ifdef USE_WIFI_CONNECT_STATE_LISTENERS // Deferred until state machine reaches STA_CONNECTED so wifi.connected // condition returns true in listener automations. diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 56fc83f12fd..4911bdde74f 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -518,7 +518,8 @@ class Scheduler { uint32_t hash_or_id, SchedulerItem::Type type, bool find_first); - Mutex lock_; + // Empty on single-threaded platforms (ESP8266, RP2), so it takes no space there + [[no_unique_address]] Mutex lock_; std::vector items_; std::vector to_add_; diff --git a/tests/component_tests/homeassistant/__init__.py b/tests/component_tests/homeassistant/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/component_tests/homeassistant/test_number.py b/tests/component_tests/homeassistant/test_number.py new file mode 100644 index 00000000000..d17842307d0 --- /dev/null +++ b/tests/component_tests/homeassistant/test_number.py @@ -0,0 +1,32 @@ +"""Tests for the homeassistant number platform.""" + +from collections.abc import Callable +from pathlib import Path + +import pytest + +from esphome.config import read_config +from esphome.core import CORE + +VALID_CONFIG = "tests/component_tests/homeassistant/test_number.yaml" +INVALID_CONFIG = "tests/component_tests/homeassistant/test_number_invalid_domain.yaml" + + +def test_number_and_input_number_entities( + generate_main: Callable[[str | Path], str], +) -> None: + """Both number and input_number entity IDs are accepted and passed to the C++ class.""" + main_cpp = generate_main(VALID_CONFIG) + + assert 'ha_number->set_entity_id("number.some_number");' in main_cpp + assert 'ha_input_number->set_entity_id("input_number.some_input");' in main_cpp + + +def test_number_rejects_unsupported_domain( + capsys: pytest.CaptureFixture[str], +) -> None: + """An entity ID from another domain fails config validation.""" + CORE.config_path = Path(INVALID_CONFIG) + + assert read_config({}) is None + assert "is not supported by the number platform" in capsys.readouterr().out diff --git a/tests/component_tests/homeassistant/test_number.yaml b/tests/component_tests/homeassistant/test_number.yaml new file mode 100644 index 00000000000..bcd8365495e --- /dev/null +++ b/tests/component_tests/homeassistant/test_number.yaml @@ -0,0 +1,17 @@ +--- +esphome: + name: test + +host: + +logger: + +api: + +number: + - platform: homeassistant + entity_id: number.some_number + id: ha_number + - platform: homeassistant + entity_id: input_number.some_input + id: ha_input_number diff --git a/tests/component_tests/homeassistant/test_number_invalid_domain.yaml b/tests/component_tests/homeassistant/test_number_invalid_domain.yaml new file mode 100644 index 00000000000..684de606205 --- /dev/null +++ b/tests/component_tests/homeassistant/test_number_invalid_domain.yaml @@ -0,0 +1,17 @@ +--- +esphome: + name: test + +host: + +logger: + +api: + +number: + - platform: homeassistant + entity_id: number.some_number + id: ha_number + - platform: homeassistant + entity_id: sensor.foo + id: ha_other_number diff --git a/tests/component_tests/ota/test_esphome_ota.py b/tests/component_tests/ota/test_esphome_ota.py index cd66503c1bb..77db1499231 100644 --- a/tests/component_tests/ota/test_esphome_ota.py +++ b/tests/component_tests/ota/test_esphome_ota.py @@ -560,7 +560,7 @@ def test_encryption_offer_codegen( own_key = encrypted and "USE_OTA_ENCRYPTION_PROVISIONED" not in defines_present assert ("esphome_esphomeotacomponent_id->set_noise_psk(" in main_cpp) is own_key # The api shares the ota's array instead of emitting the same key twice - assert main_cpp.count("_psk[] PROGMEM") == (1 if own_key else 0) + assert main_cpp.count("noise_psk[] PROGMEM") == (1 if own_key else 0) assert ("set_auth_password(" in main_cpp) is ("USE_OTA_PASSWORD" in defines_present) # The noise transport source compiles only when the define is set assert FILTER_SOURCE_FILES() == ([] if encrypted else ["ota_esphome_noise.cpp"]) diff --git a/tests/components/homeassistant/common.yaml b/tests/components/homeassistant/common.yaml index 1099f7ea85e..9bc0defae27 100644 --- a/tests/components/homeassistant/common.yaml +++ b/tests/components/homeassistant/common.yaml @@ -71,6 +71,9 @@ number: - platform: homeassistant entity_id: number.hello_world id: ha_hello_world_number + - platform: homeassistant + entity_id: input_number.hello_world + id: ha_hello_world_input_number sensor: - platform: homeassistant diff --git a/tests/integration/fixtures/api_homeassistant.yaml b/tests/integration/fixtures/api_homeassistant.yaml index 2d77821ff33..deea19bdeb2 100644 --- a/tests/integration/fixtures/api_homeassistant.yaml +++ b/tests/integration/fixtures/api_homeassistant.yaml @@ -21,6 +21,12 @@ api: - number.set: id: ha_number value: 42.5 + - number.set: + id: ha_real_number + value: 12.5 + - number.set: + id: ha_real_input_number + value: 7 - switch.turn_on: ha_switch - switch.turn_off: ha_switch - logger.log: "=== All tests completed ===" @@ -129,6 +135,13 @@ text_sensor: # Number component for testing HA number control number: + # Real homeassistant numbers: the action used depends on the entity domain + - platform: homeassistant + id: ha_real_number + entity_id: number.test_real_number + - platform: homeassistant + id: ha_real_input_number + entity_id: input_number.test_real_input_number - platform: template name: "HA Controlled Number" id: ha_number diff --git a/tests/integration/test_api_homeassistant.py b/tests/integration/test_api_homeassistant.py index b4adedf8738..bb99b418135 100644 --- a/tests/integration/test_api_homeassistant.py +++ b/tests/integration/test_api_homeassistant.py @@ -86,6 +86,15 @@ async def test_api_homeassistant( "nonexistent.action_for_error_test": loop.create_future(), # error_test_call } + # Futures keyed by (service, entity_id) for the real homeassistant numbers + ha_real_number_futures = { + ("number.set_value", "number.test_real_number"): loop.create_future(), + ( + "input_number.set_value", + "input_number.test_real_input_number", + ): loop.create_future(), + } + # Future for error message test action_error_received_future = loop.create_future() @@ -102,6 +111,10 @@ async def test_api_homeassistant( if not future.done(): future.set_result(service_call) + key = (service_call.service, service_call.data.get("entity_id")) + if key in ha_real_number_futures and not ha_real_number_futures[key].done(): + ha_real_number_futures[key].set_result(service_call) + # Immediately respond to the error test call so the test can proceed # This needs to happen synchronously so ESPHome receives the response # before logging "=== All tests completed ===" @@ -201,6 +214,16 @@ async def test_api_homeassistant( # Empty state with valid entity should work (use different entity to not interfere with test) client.send_home_assistant_state("sensor.edge_case_empty_state", "", "") + # The real homeassistant numbers reject values outside min/max, which + # default to 0 until Home Assistant provides them + for entity in ( + "number.test_real_number", + "input_number.test_real_input_number", + ): + client.send_home_assistant_state(entity, "min", "0") + client.send_home_assistant_state(entity, "max", "100") + client.send_home_assistant_state(entity, "step", "0.5") + # List entities and services _, services = await client.list_entities_services() @@ -334,6 +357,21 @@ async def test_api_homeassistant( # The value might be formatted with trailing zeros assert float(number_call.data["value"]) == 42.5 + # 7b. Real homeassistant numbers pick the action from the entity domain + real_number_call = await asyncio.wait_for( + ha_real_number_futures[("number.set_value", "number.test_real_number")], + timeout=2.0, + ) + assert float(real_number_call.data["value"]) == 12.5 + + real_input_number_call = await asyncio.wait_for( + ha_real_number_futures[ + ("input_number.set_value", "input_number.test_real_input_number") + ], + timeout=2.0, + ) + assert float(real_input_number_call.data["value"]) == 7 + # 8. HA Switch service calls switch_on_call = await asyncio.wait_for( service_call_futures["switch.turn_on"], timeout=2.0