Merge remote-tracking branch 'origin/dev' into autorepeat-progmem-timing

This commit is contained in:
J. Nick Koston
2026-10-04 18:06:30 -05:00
45 changed files with 349 additions and 112 deletions
+1 -1
View File
@@ -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
+16 -1
View File
@@ -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, &reg02));
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
+3 -3
View File
@@ -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
+1 -1
View File
@@ -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")
+2 -1
View File
@@ -726,7 +726,8 @@ async def to_code(config):
)
]
glyphs = cg.static_const_array(config[CONF_RAW_GLYPH_ID], glyph_initializer)
# constexpr Glyph table in flash; on ESP8266 static_const_array would place it in RAM.
glyphs = cg.progmem_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)
+2 -1
View File
@@ -309,7 +309,8 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo
// Unknown char, skip
ESP_LOGW(TAG, "Codepoint 0x%08" PRIx32 " not found in font", code_point);
if (!this->glyphs_.empty()) {
uint8_t glyph_width = this->glyphs_[0].advance;
// Full-width read: a narrowing byte load would fault on a PROGMEM table on ESP8266.
int glyph_width = this->glyphs_[0].advance;
display->rectangle(x_at, y_start, glyph_width, this->height_, color);
x_at += glyph_width;
}
+14
View File
@@ -1,5 +1,7 @@
#pragma once
#include <type_traits>
#include "esphome/core/color.h"
#include "esphome/core/datatypes.h"
#include "esphome/core/defines.h"
@@ -36,6 +38,18 @@ class Glyph final {
int width;
int height;
};
// The glyph table lives in flash, read with plain loads, which ESP8266 only allows for whole words.
static_assert(std::is_same_v<decltype(Glyph::code_point), const uint32_t>, "Glyph fields must stay word sized");
static_assert(std::is_same_v<decltype(Glyph::data), const uint8_t *>, "Glyph fields must stay word sized");
static_assert(std::is_same_v<decltype(Glyph::advance), int>, "Glyph fields must stay word sized");
static_assert(std::is_same_v<decltype(Glyph::offset_x), int>, "Glyph fields must stay word sized");
static_assert(std::is_same_v<decltype(Glyph::offset_y), int>, "Glyph fields must stay word sized");
static_assert(std::is_same_v<decltype(Glyph::width), int>, "Glyph fields must stay word sized");
static_assert(std::is_same_v<decltype(Glyph::height), int>, "Glyph fields must stay word sized");
#ifdef USE_ESP8266
static_assert(alignof(Glyph) == sizeof(uint32_t), "Glyph fields must stay word sized");
static_assert(sizeof(Glyph) == 7 * sizeof(uint32_t), "Glyph is read from flash with word loads");
#endif
class Font final
#ifdef USE_DISPLAY
@@ -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),
)
@@ -1,5 +1,7 @@
#include "homeassistant_number.h"
#include <cstring>
#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();
@@ -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 "";
}
@@ -268,6 +268,11 @@ class HttpContainer : public Parented<HttpRequestComponent> {
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:
@@ -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};
@@ -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<uint8_t> 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<void(improv::State, improv::Error)> 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_();
+4 -1
View File
@@ -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<Deduplicator<uint8_t>, 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<text_sensor::TextSensor *, MAX_TARGETS> direction_text_sensors_{};
std::array<Deduplicator<uint8_t>, MAX_TARGETS> direction_dedup_{};
#endif
LazyCallbackManager<void()> data_callback_;
+1 -1
View File
@@ -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
+9
View File
@@ -48,6 +48,7 @@ from esphome.cpp_generator import MockObj
from esphome.final_validate import full_config
from esphome.helpers import write_file_if_changed
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from esphome.types import ConfigType
from esphome.writer import clean_build
from esphome.yaml_util import load_yaml
@@ -641,7 +642,15 @@ LVGL_TOP_LEVEL_SCHEMA = (
)
def _not_on_esp8266(config: ConfigType) -> ConfigType:
# ESP8266 does not have enough RAM for LVGL to be practical.
if CORE.is_esp8266:
raise cv.Invalid("LVGL is not supported on ESP8266")
return config
LVGL_SCHEMA = cv.All(
_not_on_esp8266,
container_schema(obj_spec, LVGL_TOP_LEVEL_SCHEMA),
cv.has_at_most_one_key(CONF_PAGES, df.CONF_LAYOUT),
add_hello_world,
+2 -2
View File
@@ -1037,7 +1037,7 @@ void lv_mem_init() {}
void lv_mem_deinit() {}
#if defined(USE_HOST) || defined(USE_RP2) || defined(USE_ESP8266)
#ifndef USE_ESP32
void *lv_malloc_core(size_t size) {
auto *ptr = malloc(size); // NOLINT
if (ptr == nullptr) {
@@ -1053,7 +1053,7 @@ static void *lv_alloc_draw_buf(size_t size, bool internal) {
return malloc(size); // NOLINT
}
#elif defined(USE_ESP32)
#else
static unsigned cap_bits = MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT; // NOLINT
static void *lv_alloc_draw_buf(size_t size, bool internal) {
@@ -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<std::vector<uint8_t>> 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
@@ -107,12 +107,12 @@ class SourceSpeaker final : public speaker::Speaker, public Component {
bool stop_gracefully_{false};
bool pause_state_{false};
std::atomic<bool> has_contributed_{false}; // Tracks if source has contributed during this session
esp_audio_libs::gain::GainRamp ducking_ramp_;
std::atomic<uint32_t> pending_playback_frames_{0};
std::atomic<uint32_t> playback_delay_frames_{0}; // Frames in output pipeline when this source started contributing
std::atomic<bool> has_contributed_{false}; // Tracks if source has contributed during this session
EventGroupHandle_t event_group_{nullptr};
uint32_t stopping_start_ms_{0};
+5 -11
View File
@@ -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:
@@ -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
+23 -17
View File
@@ -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<uint8_t> allocator;
allocator.deallocate(this->storage_, this->size_);
}
@@ -31,7 +32,12 @@ std::unique_ptr<RingBuffer> 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> 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;
}
}
+3 -1
View File
@@ -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<StaticRingbuffer_t *>(&this->structure_); }
StaticRingbuffer_t structure_;
uint8_t *storage_{nullptr};
size_t size_{0};
+2 -2
View File
@@ -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.
+2 -1
View File
@@ -254,7 +254,8 @@ template<class... As, typename... Ts> class ScriptExecuteAction<Script<As...>, T
}
Script<As...> *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.
+3 -1
View File
@@ -308,6 +308,9 @@ class SendspinHub final : public Component,
CallbackManager<void(const sendspin::GroupUpdateObject &)> 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<bool> 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
};
+1 -1
View File
@@ -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};
@@ -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<float> set_trigger_;
TemplateLambda<float> f_;
@@ -9,7 +9,8 @@
namespace esphome::template_ {
struct Empty {};
// Distinct per member: two empty members of the same type cannot share an address
template<int N> 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<HAS_LAMBDA, TemplateLambda<std::string>, Empty> f_{};
[[no_unique_address]] std::conditional_t<RESTORE_VALUE, ESPPreferenceObject, Empty> pref_{};
[[no_unique_address]] std::conditional_t<HAS_LAMBDA, TemplateLambda<std::string>, Empty<0>> f_{};
[[no_unique_address]] std::conditional_t<RESTORE_VALUE, ESPPreferenceObject, Empty<1>> pref_{};
};
/// Template select with set_action trigger - only instantiated when set_action is configured
@@ -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> timer_updated_trigger_;
Trigger<Timer> timer_cancelled_trigger_;
Trigger<const std::vector<Timer> &> 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)
+6 -5
View File
@@ -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<WiFiIPStateListener *, ESPHOME_WIFI_IP_STATE_LISTENERS> 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.
+2 -1
View File
@@ -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<SchedulerItem *> items_;
std::vector<SchedulerItem *> to_add_;
@@ -0,0 +1,11 @@
esphome:
name: test
esp8266:
board: esp01_1m
font:
- file: ../NotoSans-Regular.ttf
id: noto
size: 12
glyphs: "AB"
+20 -10
View File
@@ -4,8 +4,10 @@ Focuses on verifying that long multi-byte (Chinese/CJK) glyph strings
are correctly processed through the font configuration pipeline.
"""
from collections.abc import Callable
import functools
from pathlib import Path
import re
from unittest.mock import MagicMock, patch
import pytest
@@ -260,15 +262,12 @@ def mock_cg():
with (
patch("esphome.components.font.cg.add_define") as mock_define,
patch("esphome.components.font.cg.progmem_array") as mock_progmem,
patch("esphome.components.font.cg.static_const_array") as mock_static,
patch("esphome.components.font.cg.new_Pvariable") as mock_new_pvar,
):
mock_progmem.return_value = MagicMock()
mock_static.return_value = MagicMock()
yield {
"add_define": mock_define,
"progmem_array": mock_progmem,
"static_const_array": mock_static,
"new_Pvariable": mock_new_pvar,
}
@@ -288,13 +287,13 @@ async def test_to_code_long_latin_generates_all_glyphs(mock_cg):
mock_cg["add_define"].assert_any_call("USE_FONT")
# progmem_array receives the combined bitmap data (non-empty)
mock_cg["progmem_array"].assert_called_once()
bitmap_data = mock_cg["progmem_array"].call_args.args[1]
# bitmap data first, then the glyph table
assert mock_cg["progmem_array"].call_count == 2
bitmap_data = mock_cg["progmem_array"].call_args_list[0].args[1]
assert len(bitmap_data) > 0
# static_const_array receives one entry per unique glyph
mock_cg["static_const_array"].assert_called_once()
glyph_initializer = mock_cg["static_const_array"].call_args.args[1]
# the glyph table receives one entry per unique glyph
glyph_initializer = mock_cg["progmem_array"].call_args_list[1].args[1]
assert len(glyph_initializer) == glyph_count
# new_Pvariable is called with the correct glyph count
@@ -314,7 +313,7 @@ async def test_to_code_glyph_entries_contain_expected_fields(mock_cg):
await to_code(config)
glyph_initializer = mock_cg["static_const_array"].call_args.args[1]
glyph_initializer = mock_cg["progmem_array"].call_args_list[1].args[1]
for entry in glyph_initializer:
assert len(entry) == 7, f"Glyph entry should have 7 fields, got {len(entry)}"
codepoint = entry[0]
@@ -332,6 +331,17 @@ async def test_to_code_glyphs_sorted_by_utf8(mock_cg):
await to_code(config)
glyph_initializer = mock_cg["static_const_array"].call_args.args[1]
glyph_initializer = mock_cg["progmem_array"].call_args_list[1].args[1]
codepoints = [entry[0] for entry in glyph_initializer]
assert codepoints == sorted(codepoints)
def test_glyph_table_is_progmem(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""The constexpr glyph table goes to flash, not a RAM static const array."""
main_cpp = generate_main(component_config_path("glyph_table.yaml"))
assert re.search(r"static constexpr font::Glyph \w+\[\] PROGMEM = ", main_cpp)
assert "static const font::Glyph" not in main_cpp
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,14 @@
"""LVGL rejects ESP8266 at validation time."""
import pytest
from esphome.components import lvgl
from esphome.config_validation import Invalid
from esphome.const import PlatformFramework
from tests.component_tests.types import SetCoreConfigCallable
def test_esp8266_is_rejected(set_core_config: SetCoreConfigCallable) -> None:
set_core_config(PlatformFramework.ESP8266_ARDUINO)
with pytest.raises(Invalid, match="not supported on ESP8266"):
lvgl.CONFIG_SCHEMA({})
@@ -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"])
@@ -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
@@ -3,5 +3,7 @@ button:
name: Clear motion calibration
on_press:
- motion.clear_calibration:
id: qmi8658_motion
- motion.clear_calibration:
id: qmi8658_motion
save: true
+12
View File
@@ -3,6 +3,7 @@ sensor:
name: "QMI8658 Temperature"
- platform: motion
motion_id: qmi8658_motion
type: acceleration_x
name: "Accel X"
accuracy_decimals: 4
@@ -11,44 +12,55 @@ sensor:
window_size: 4
send_every: 1
- platform: motion
motion_id: qmi8658_motion
type: acceleration_y
name: "Accel Y"
accuracy_decimals: 4
- platform: motion
motion_id: qmi8658_motion
type: acceleration_z
name: "Accel Z"
accuracy_decimals: 4
# Gyroscope axes (unit: °/s)
- platform: motion
motion_id: qmi8658_motion
type: gyroscope_x
name: "Gyro X"
- platform: motion
motion_id: qmi8658_motion
type: gyroscope_y
name: "Gyro Y"
- platform: motion
motion_id: qmi8658_motion
type: gyroscope_z
name: "Gyro Z"
- platform: motion
motion_id: qmi8658_motion
type: angular_rate_x
name: "Angular Rate X"
- platform: motion
motion_id: qmi8658_motion
type: angular_rate_y
name: "Angular Rate Y"
- platform: motion
motion_id: qmi8658_motion
type: angular_rate_z
name: "Angular Rate Z"
- platform: motion
motion_id: qmi8658_motion
type: pitch
name: "Pitch"
- platform: motion
motion_id: qmi8658_motion
type: roll
name: "Roll"
motion:
- platform: qmi8658
id: qmi8658_motion
i2c_id: i2c_bus
# Accelerometer full-scale range: 2G | 4G | 8G | 16G
accelerometer_range: 4G
@@ -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
@@ -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