Compare commits

..

18 Commits

Author SHA1 Message Date
J. Nick Koston
d663ea56b0 tidy 2025-11-05 11:00:02 -06:00
J. Nick Koston
5372eca46e [mqtt] Use StringRef to avoid string copies in discovery 2025-11-05 10:43:22 -06:00
J. Nick Koston
479f8dd85c [rtttl] Reduce flash usage by eliminating substr() allocations (#11722) 2025-11-05 09:17:28 -06:00
J. Nick Koston
6e2dbbf636 [voice_assistant] Eliminate substr() allocations in text truncation (#11725) 2025-11-05 09:15:05 -06:00
J. Nick Koston
6b522dfee6 [wifi_info] Reduce heap usage by up to 1.7KB in scan_results sensor (#11723) 2025-11-05 09:14:21 -06:00
J. Nick Koston
32975c9d8b [select][lvgl] Fix FixedVector size() returning 0 when using operator[] after init() (#11721) 2025-11-05 01:49:27 +00:00
J. Nick Koston
1446e7174a [core] Reduce action framework argument copies by 83% (#11704)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-11-05 01:23:24 +00:00
Gnuspice
64f8963566 [const] Move CONF_ENABLED to const.py (#11719) 2025-11-05 12:46:06 +13:00
J. Nick Koston
6f7e54c3f3 [select] Refactor to index-based operations for immediate and future RAM savings (#11623) 2025-11-05 11:33:01 +13:00
J. Nick Koston
c7ae424613 [display] Optimize display writers with function pointers for stateless lambdas (#11629) 2025-11-05 11:14:54 +13:00
Clyde Stubbs
c5e5609e92 [lvgl] Fix case sensitivity in flex layout (#11717) 2025-11-05 09:00:12 +11:00
J. Nick Koston
885508775f [fan] Remove duplicate preset mode storage to save RAM (#11632) 2025-11-05 10:55:37 +13:00
J. Nick Koston
531b27582a [network] Store use_address in RODATA to save RAM (#11707) 2025-11-05 10:52:10 +13:00
J. Nick Koston
aed7505f53 [automations] Reduce memory usage in if/while/repeat actions (32-36 bytes per instance) (#11650)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-11-05 10:48:20 +13:00
Javier Peletier
191a88c2dc [gt911] Fix gt911 touchscreen with reset pin not initializing when loglevel is set to NONE (#11715) 2025-11-04 13:38:59 -05:00
SeByDocKy
968df6cb3f [gp8403] Add gp8413 (15 bits) DAC model (#7726)
Co-authored-by: Djordje Mandic <6750655+DjordjeMandic@users.noreply.github.com>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-11-04 12:16:11 -05:00
Cameron Steel
71fa88c9d4 [max7219digit] support flip_x when rotate_chip is 90° or 270° (#6109)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-11-04 16:32:23 +00:00
Chaser Huang
84f7cacef9 [sgp30] Fix reading from preexisting stored baseline even with store_baseline:false (#7922)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-11-04 15:41:30 +00:00
72 changed files with 389 additions and 360 deletions

View File

@@ -181,7 +181,7 @@ esphome/components/gdk101/* @Szewcson
esphome/components/gl_r01_i2c/* @pkejval
esphome/components/globals/* @esphome/core
esphome/components/gp2y1010au0f/* @zry98
esphome/components/gp8403/* @jesserockz
esphome/components/gp8403/* @jesserockz @sebydocky
esphome/components/gpio/* @esphome/core
esphome/components/gpio/one_wire/* @ssieb
esphome/components/gps/* @coogle @ximex

View File

@@ -741,13 +741,6 @@ def command_vscode(args: ArgsProtocol) -> int | None:
def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None:
# Set memory analysis options in config
if args.analyze_memory:
config.setdefault(CONF_ESPHOME, {})["analyze_memory"] = True
if args.memory_report:
config.setdefault(CONF_ESPHOME, {})["memory_report_file"] = args.memory_report
exit_code = write_cpp(config)
if exit_code != 0:
return exit_code
@@ -1209,17 +1202,6 @@ def parse_args(argv):
help="Only generate source code, do not compile.",
action="store_true",
)
parser_compile.add_argument(
"--analyze-memory",
help="Analyze and display memory usage by component after compilation.",
action="store_true",
)
parser_compile.add_argument(
"--memory-report",
help="Save memory analysis report to a file (supports .json or .txt).",
type=str,
metavar="FILE",
)
parser_upload = subparsers.add_parser(
"upload",

View File

@@ -1,7 +1,6 @@
"""CLI interface for memory analysis with report generation."""
from collections import defaultdict
import json
import sys
from . import (
@@ -284,28 +283,6 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
return "\n".join(lines)
def to_json(self) -> str:
"""Export analysis results as JSON."""
data = {
"components": {
name: {
"text": mem.text_size,
"rodata": mem.rodata_size,
"data": mem.data_size,
"bss": mem.bss_size,
"flash_total": mem.flash_total,
"ram_total": mem.ram_total,
"symbol_count": mem.symbol_count,
}
for name, mem in self.components.items()
},
"totals": {
"flash": sum(c.flash_total for c in self.components.values()),
"ram": sum(c.ram_total for c in self.components.values()),
},
}
return json.dumps(data, indent=2)
def dump_uncategorized_symbols(self, output_file: str | None = None) -> None:
"""Dump uncategorized symbols for analysis."""
# Sort by size descending

View File

@@ -8,6 +8,7 @@ BYTE_ORDER_BIG = "big_endian"
CONF_COLOR_DEPTH = "color_depth"
CONF_DRAW_ROUNDING = "draw_rounding"
CONF_ENABLED = "enabled"
CONF_ON_RECEIVE = "on_receive"
CONF_ON_STATE_CHANGE = "on_state_change"
CONF_REQUEST_HEADERS = "request_headers"

View File

@@ -176,7 +176,117 @@ class Display;
class DisplayPage;
class DisplayOnPageChangeTrigger;
using display_writer_t = std::function<void(Display &)>;
/** Optimized display writer that uses function pointers for stateless lambdas.
*
* Similar to TemplatableValue but specialized for display writer callbacks.
* Saves ~8 bytes per stateless lambda on 32-bit platforms (16 bytes std::function → ~8 bytes discriminator+pointer).
*
* Supports both:
* - Stateless lambdas (from YAML) → function pointer (4 bytes)
* - Stateful lambdas/std::function (from C++ code) → std::function* (heap allocated)
*
* @tparam T The display type (e.g., Display, Nextion, GPIOLCDDisplay)
*/
template<typename T> class DisplayWriter {
public:
DisplayWriter() : type_(NONE) {}
// For stateless lambdas (convertible to function pointer): use function pointer (4 bytes)
template<typename F>
DisplayWriter(F f) requires std::invocable<F, T &> && std::convertible_to<F, void (*)(T &)>
: type_(STATELESS_LAMBDA) {
this->stateless_f_ = f; // Implicit conversion to function pointer
}
// For stateful lambdas and std::function (not convertible to function pointer): use std::function* (heap allocated)
// This handles backwards compatibility with external components
template<typename F>
DisplayWriter(F f) requires std::invocable<F, T &> &&(!std::convertible_to<F, void (*)(T &)>) : type_(LAMBDA) {
this->f_ = new std::function<void(T &)>(std::move(f));
}
// Copy constructor
DisplayWriter(const DisplayWriter &other) : type_(other.type_) {
if (type_ == LAMBDA) {
this->f_ = new std::function<void(T &)>(*other.f_);
} else if (type_ == STATELESS_LAMBDA) {
this->stateless_f_ = other.stateless_f_;
}
}
// Move constructor
DisplayWriter(DisplayWriter &&other) noexcept : type_(other.type_) {
if (type_ == LAMBDA) {
this->f_ = other.f_;
other.f_ = nullptr;
} else if (type_ == STATELESS_LAMBDA) {
this->stateless_f_ = other.stateless_f_;
}
other.type_ = NONE;
}
// Assignment operators
DisplayWriter &operator=(const DisplayWriter &other) {
if (this != &other) {
this->~DisplayWriter();
new (this) DisplayWriter(other);
}
return *this;
}
DisplayWriter &operator=(DisplayWriter &&other) noexcept {
if (this != &other) {
this->~DisplayWriter();
new (this) DisplayWriter(std::move(other));
}
return *this;
}
~DisplayWriter() {
if (type_ == LAMBDA) {
delete this->f_;
}
// STATELESS_LAMBDA/NONE: no cleanup needed (function pointer or empty)
}
bool has_value() const { return this->type_ != NONE; }
void call(T &display) const {
switch (this->type_) {
case STATELESS_LAMBDA:
this->stateless_f_(display); // Direct function pointer call
break;
case LAMBDA:
(*this->f_)(display); // std::function call
break;
case NONE:
default:
break;
}
}
// Operator() for convenience
void operator()(T &display) const { this->call(display); }
// Operator* for backwards compatibility with (*writer_)(*this) pattern
DisplayWriter &operator*() { return *this; }
const DisplayWriter &operator*() const { return *this; }
protected:
enum : uint8_t {
NONE,
LAMBDA,
STATELESS_LAMBDA,
} type_;
union {
std::function<void(T &)> *f_;
void (*stateless_f_)(T &);
};
};
// Type alias for Display writer - uses optimized DisplayWriter instead of std::function
using display_writer_t = DisplayWriter<Display>;
#define LOG_DISPLAY(prefix, type, obj) \
if ((obj) != nullptr) { \
@@ -678,7 +788,7 @@ class Display : public PollingComponent {
void sort_triangle_points_by_y_(int *x1, int *y1, int *x2, int *y2, int *x3, int *y3);
DisplayRotation rotation_{DISPLAY_ROTATION_0_DEGREES};
optional<display_writer_t> writer_{};
display_writer_t writer_{};
DisplayPage *page_{nullptr};
DisplayPage *previous_page_{nullptr};
std::vector<DisplayOnPageChangeTrigger *> on_page_change_triggers_;

View File

@@ -96,10 +96,6 @@ void ESP32BLE::advertising_set_service_data(const std::vector<uint8_t> &data) {
}
void ESP32BLE::advertising_set_manufacturer_data(const std::vector<uint8_t> &data) {
this->advertising_set_manufacturer_data(std::span<const uint8_t>(data));
}
void ESP32BLE::advertising_set_manufacturer_data(std::span<const uint8_t> data) {
this->advertising_init_();
this->advertising_->set_manufacturer_data(data);
this->advertising_start();

View File

@@ -118,7 +118,6 @@ class ESP32BLE : public Component {
void advertising_start();
void advertising_set_service_data(const std::vector<uint8_t> &data);
void advertising_set_manufacturer_data(const std::vector<uint8_t> &data);
void advertising_set_manufacturer_data(std::span<const uint8_t> data);
void advertising_set_appearance(uint16_t appearance) { this->appearance_ = appearance; }
void advertising_set_service_data_and_name(std::span<const uint8_t> data, bool include_name);
void advertising_add_service_uuid(ESPBTUUID uuid);

View File

@@ -59,10 +59,6 @@ void BLEAdvertising::set_service_data(const std::vector<uint8_t> &data) {
}
void BLEAdvertising::set_manufacturer_data(const std::vector<uint8_t> &data) {
this->set_manufacturer_data(std::span<const uint8_t>(data));
}
void BLEAdvertising::set_manufacturer_data(std::span<const uint8_t> data) {
delete[] this->advertising_data_.p_manufacturer_data;
this->advertising_data_.p_manufacturer_data = nullptr;
this->advertising_data_.manufacturer_len = data.size();

View File

@@ -37,7 +37,6 @@ class BLEAdvertising {
void set_scan_response(bool scan_response) { this->scan_response_ = scan_response; }
void set_min_preferred_interval(uint16_t interval) { this->advertising_data_.min_interval = interval; }
void set_manufacturer_data(const std::vector<uint8_t> &data);
void set_manufacturer_data(std::span<const uint8_t> data);
void set_appearance(uint16_t appearance) { this->advertising_data_.appearance = appearance; }
void set_service_data(const std::vector<uint8_t> &data);
void set_service_data(std::span<const uint8_t> data);

View File

@@ -1,6 +1,5 @@
#include "esp32_ble_beacon.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
#ifdef USE_ESP32

View File

@@ -15,10 +15,7 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_characteristic_on_w
Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory)
new Trigger<std::vector<uint8_t>, uint16_t>();
characteristic->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) {
// Convert span to vector for trigger - copy is necessary because:
// 1. Trigger stores the data for use in automation actions that execute later
// 2. The span is only valid during this callback (points to temporary BLE stack data)
// 3. User lambdas in automations need persistent data they can access asynchronously
// Convert span to vector for trigger
on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id);
});
return on_write_trigger;
@@ -30,10 +27,7 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_descriptor_on_write
Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory)
new Trigger<std::vector<uint8_t>, uint16_t>();
descriptor->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) {
// Convert span to vector for trigger - copy is necessary because:
// 1. Trigger stores the data for use in automation actions that execute later
// 2. The span is only valid during this callback (points to temporary BLE stack data)
// 3. User lambdas in automations need persistent data they can access asynchronously
// Convert span to vector for trigger
on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id);
});
return on_write_trigger;

View File

@@ -231,7 +231,7 @@ void Fan::save_state_() {
state.direction = this->direction;
const char *preset = this->get_preset_mode();
if (traits.supports_preset_modes() && preset != nullptr) {
if (preset != nullptr) {
const auto &preset_modes = traits.supported_preset_modes();
// Find index of current preset mode (pointer comparison is safe since preset is from traits)
for (size_t i = 0; i < preset_modes.size(); i++) {

View File

@@ -1,19 +1,25 @@
import esphome.codegen as cg
from esphome.components import i2c
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_VOLTAGE
from esphome.const import CONF_ID, CONF_MODEL, CONF_VOLTAGE
CODEOWNERS = ["@jesserockz"]
CODEOWNERS = ["@jesserockz", "@sebydocky"]
DEPENDENCIES = ["i2c"]
MULTI_CONF = True
gp8403_ns = cg.esphome_ns.namespace("gp8403")
GP8403 = gp8403_ns.class_("GP8403", cg.Component, i2c.I2CDevice)
GP8403Component = gp8403_ns.class_("GP8403Component", cg.Component, i2c.I2CDevice)
GP8403Voltage = gp8403_ns.enum("GP8403Voltage")
GP8403Model = gp8403_ns.enum("GP8403Model")
CONF_GP8403_ID = "gp8403_id"
MODELS = {
"GP8403": GP8403Model.GP8403,
"GP8413": GP8403Model.GP8413,
}
VOLTAGES = {
"5V": GP8403Voltage.GP8403_VOLTAGE_5V,
"10V": GP8403Voltage.GP8403_VOLTAGE_10V,
@@ -22,7 +28,8 @@ VOLTAGES = {
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(GP8403),
cv.GenerateID(): cv.declare_id(GP8403Component),
cv.Optional(CONF_MODEL, default="GP8403"): cv.enum(MODELS, upper=True),
cv.Required(CONF_VOLTAGE): cv.enum(VOLTAGES, upper=True),
}
)
@@ -35,5 +42,5 @@ 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_model(config[CONF_MODEL]))
cg.add(var.set_voltage(config[CONF_VOLTAGE]))

View File

@@ -8,16 +8,48 @@ namespace gp8403 {
static const char *const TAG = "gp8403";
static const uint8_t RANGE_REGISTER = 0x01;
static const uint8_t OUTPUT_REGISTER = 0x02;
void GP8403::setup() { this->write_register(RANGE_REGISTER, (uint8_t *) (&this->voltage_), 1); }
const LogString *model_to_string(GP8403Model model) {
switch (model) {
case GP8403Model::GP8403:
return LOG_STR("GP8403");
case GP8403Model::GP8413:
return LOG_STR("GP8413");
}
return LOG_STR("Unknown");
};
void GP8403::dump_config() {
void GP8403Component::setup() { this->write_register(RANGE_REGISTER, (uint8_t *) (&this->voltage_), 1); }
void GP8403Component::dump_config() {
ESP_LOGCONFIG(TAG,
"GP8403:\n"
" Voltage: %dV",
this->voltage_ == GP8403_VOLTAGE_5V ? 5 : 10);
" Voltage: %dV\n"
" Model: %s",
this->voltage_ == GP8403_VOLTAGE_5V ? 5 : 10, LOG_STR_ARG(model_to_string(this->model_)));
LOG_I2C_DEVICE(this);
}
void GP8403Component::write_state(float state, uint8_t channel) {
uint16_t val = 0;
switch (this->model_) {
case GP8403Model::GP8403:
val = ((uint16_t) (4095 * state)) << 4;
break;
case GP8403Model::GP8413:
val = ((uint16_t) (32767 * state)) << 1;
break;
default:
ESP_LOGE(TAG, "Unknown model %s", LOG_STR_ARG(model_to_string(this->model_)));
return;
}
ESP_LOGV(TAG, "Calculated DAC value: %" PRIu16, val);
i2c::ErrorCode err = this->write_register(OUTPUT_REGISTER + (2 * channel), (uint8_t *) &val, 2);
if (err != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Error writing to %s, code %d", LOG_STR_ARG(model_to_string(this->model_)), err);
}
}
} // namespace gp8403
} // namespace esphome

View File

@@ -11,15 +11,24 @@ enum GP8403Voltage {
GP8403_VOLTAGE_10V = 0x11,
};
class GP8403 : public Component, public i2c::I2CDevice {
enum GP8403Model {
GP8403,
GP8413,
};
class GP8403Component : 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_model(GP8403Model model) { this->model_ = model; }
void set_voltage(gp8403::GP8403Voltage voltage) { this->voltage_ = voltage; }
void write_state(float state, uint8_t channel);
protected:
GP8403Voltage voltage_;
GP8403Model model_{GP8403Model::GP8403};
};
} // namespace gp8403

View File

@@ -3,7 +3,7 @@ from esphome.components import i2c, output
import esphome.config_validation as cv
from esphome.const import CONF_CHANNEL, CONF_ID
from .. import CONF_GP8403_ID, GP8403, gp8403_ns
from .. import CONF_GP8403_ID, GP8403Component, gp8403_ns
DEPENDENCIES = ["gp8403"]
@@ -14,7 +14,7 @@ GP8403Output = gp8403_ns.class_(
CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(GP8403Output),
cv.GenerateID(CONF_GP8403_ID): cv.use_id(GP8403),
cv.GenerateID(CONF_GP8403_ID): cv.use_id(GP8403Component),
cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=1),
}
).extend(cv.COMPONENT_SCHEMA)

View File

@@ -7,8 +7,6 @@ namespace gp8403 {
static const char *const TAG = "gp8403.output";
static const uint8_t OUTPUT_REGISTER = 0x02;
void GP8403Output::dump_config() {
ESP_LOGCONFIG(TAG,
"GP8403 Output:\n"
@@ -16,13 +14,7 @@ void GP8403Output::dump_config() {
this->channel_);
}
void GP8403Output::write_state(float state) {
uint16_t value = ((uint16_t) (state * 4095)) << 4;
i2c::ErrorCode err = this->parent_->write_register(OUTPUT_REGISTER + (2 * this->channel_), (uint8_t *) &value, 2);
if (err != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Error writing to GP8403, code %d", err);
}
}
void GP8403Output::write_state(float state) { this->parent_->write_state(state, this->channel_); }
} // namespace gp8403
} // namespace esphome

View File

@@ -8,13 +8,11 @@
namespace esphome {
namespace gp8403 {
class GP8403Output : public Component, public output::FloatOutput, public Parented<GP8403> {
class GP8403Output : public Component, public output::FloatOutput, public Parented<GP8403Component> {
public:
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA - 1; }
void set_channel(uint8_t channel) { this->channel_ = channel; }
void write_state(float state) override;
protected:

View File

@@ -34,8 +34,8 @@ void GT911Touchscreen::setup() {
this->interrupt_pin_->digital_write(false);
}
delay(2);
this->reset_pin_->digital_write(true); // wait 50ms after reset
this->set_timeout(50, [this] { this->setup_internal_(); });
this->reset_pin_->digital_write(true); // wait at least T3+T4 ms as per the datasheet
this->set_timeout(5 + 50 + 1, [this] { this->setup_internal_(); });
return;
}
this->setup_internal_();

View File

@@ -107,7 +107,7 @@ void IDFI2CBus::dump_config() {
if (s.second) {
ESP_LOGCONFIG(TAG, "Found device at address 0x%02X", s.first);
} else {
ESP_LOGCONFIG(TAG, "Unknown error at address 0x%02X", s.first);
ESP_LOGE(TAG, "Unknown error at address 0x%02X", s.first);
}
}
}

View File

@@ -2,13 +2,18 @@
#include "esphome/core/hal.h"
#include "esphome/components/lcd_base/lcd_display.h"
#include "esphome/components/display/display.h"
namespace esphome {
namespace lcd_gpio {
class GPIOLCDDisplay;
using gpio_lcd_writer_t = display::DisplayWriter<GPIOLCDDisplay>;
class GPIOLCDDisplay : public lcd_base::LCDDisplay {
public:
void set_writer(std::function<void(GPIOLCDDisplay &)> &&writer) { this->writer_ = std::move(writer); }
void set_writer(gpio_lcd_writer_t &&writer) { this->writer_ = std::move(writer); }
void setup() override;
void set_data_pins(GPIOPin *d0, GPIOPin *d1, GPIOPin *d2, GPIOPin *d3) {
this->data_pins_[0] = d0;
@@ -43,7 +48,7 @@ class GPIOLCDDisplay : public lcd_base::LCDDisplay {
GPIOPin *rw_pin_{nullptr};
GPIOPin *enable_pin_{nullptr};
GPIOPin *data_pins_[8]{nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr};
std::function<void(GPIOLCDDisplay &)> writer_;
gpio_lcd_writer_t writer_;
};
} // namespace lcd_gpio

View File

@@ -3,13 +3,18 @@
#include "esphome/core/component.h"
#include "esphome/components/lcd_base/lcd_display.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/display/display.h"
namespace esphome {
namespace lcd_pcf8574 {
class PCF8574LCDDisplay;
using pcf8574_lcd_writer_t = display::DisplayWriter<PCF8574LCDDisplay>;
class PCF8574LCDDisplay : public lcd_base::LCDDisplay, public i2c::I2CDevice {
public:
void set_writer(std::function<void(PCF8574LCDDisplay &)> &&writer) { this->writer_ = std::move(writer); }
void set_writer(pcf8574_lcd_writer_t &&writer) { this->writer_ = std::move(writer); }
void setup() override;
void dump_config() override;
void backlight();
@@ -24,7 +29,7 @@ class PCF8574LCDDisplay : public lcd_base::LCDDisplay, public i2c::I2CDevice {
// Stores the current state of the backlight.
uint8_t backlight_value_;
std::function<void(PCF8574LCDDisplay &)> writer_;
pcf8574_lcd_writer_t writer_;
};
} // namespace lcd_pcf8574

View File

@@ -1,4 +1,5 @@
import re
import textwrap
import esphome.config_validation as cv
from esphome.const import CONF_HEIGHT, CONF_TYPE, CONF_WIDTH
@@ -122,7 +123,7 @@ class FlexLayout(Layout):
def get_layout_schemas(self, config: dict) -> tuple:
layout = config.get(CONF_LAYOUT)
if not isinstance(layout, dict) or layout.get(CONF_TYPE) != TYPE_FLEX:
if not isinstance(layout, dict) or layout.get(CONF_TYPE).lower() != TYPE_FLEX:
return None, {}
child_schema = FLEX_OBJ_SCHEMA
if grow := layout.get(CONF_FLEX_GROW):
@@ -161,6 +162,8 @@ class DirectionalLayout(FlexLayout):
return self.direction
def get_layout_schemas(self, config: dict) -> tuple:
if not isinstance(config.get(CONF_LAYOUT), str):
return None, {}
if config.get(CONF_LAYOUT, "").lower() != self.direction:
return None, {}
return cv.one_of(self.direction, lower=True), flex_hv_schema(self.direction)
@@ -206,7 +209,7 @@ class GridLayout(Layout):
# Not a valid grid layout string
return None, {}
if not isinstance(layout, dict) or layout.get(CONF_TYPE) != TYPE_GRID:
if not isinstance(layout, dict) or layout.get(CONF_TYPE).lower() != TYPE_GRID:
return None, {}
return (
{
@@ -259,7 +262,7 @@ class GridLayout(Layout):
)
# should be guaranteed to be a dict at this point
assert isinstance(layout, dict)
assert layout.get(CONF_TYPE) == TYPE_GRID
assert layout.get(CONF_TYPE).lower() == TYPE_GRID
rows = len(layout[CONF_GRID_ROWS])
columns = len(layout[CONF_GRID_COLUMNS])
used_cells = [[None] * columns for _ in range(rows)]
@@ -335,6 +338,17 @@ def append_layout_schema(schema, config: dict):
if CONF_LAYOUT not in config:
# If no layout is specified, return the schema as is
return schema.extend({cv.Optional(CONF_WIDGETS): any_widget_schema()})
layout = config[CONF_LAYOUT]
# Sanity check the layout to avoid redundant checks in each type
if not isinstance(layout, str) and not isinstance(layout, dict):
raise cv.Invalid(
"The 'layout' option must be a string or a dictionary", [CONF_LAYOUT]
)
if isinstance(layout, dict) and not isinstance(layout.get(CONF_TYPE), str):
raise cv.Invalid(
"Invalid layout type; must be a string ('flex' or 'grid')",
[CONF_LAYOUT, CONF_TYPE],
)
for layout_class in LAYOUT_CLASSES:
layout_schema, child_schema = layout_class.get_layout_schemas(config)
@@ -348,10 +362,17 @@ def append_layout_schema(schema, config: dict):
layout_schema.add_extra(layout_class.validate)
return layout_schema.extend(schema)
# If no layout class matched, return a default schema
return cv.Schema(
{
cv.Optional(CONF_LAYOUT): cv.one_of(*LAYOUT_CHOICES, lower=True),
cv.Optional(CONF_WIDGETS): any_widget_schema(),
}
if isinstance(layout, dict):
raise cv.Invalid(
"Invalid layout type; must be 'flex' or 'grid'", [CONF_LAYOUT, CONF_TYPE]
)
raise cv.Invalid(
textwrap.dedent(
"""
Invalid 'layout' value
layout choices are 'horizontal', 'vertical', '<rows>x<cols>',
or a dictionary with a 'type' key
"""
),
[CONF_LAYOUT],
)

View File

@@ -59,8 +59,8 @@ class LVGLSelect : public select::Select, public Component {
const auto &opts = this->widget_->get_options();
FixedVector<const char *> opt_ptrs;
opt_ptrs.init(opts.size());
for (size_t i = 0; i < opts.size(); i++) {
opt_ptrs[i] = opts[i].c_str();
for (const auto &opt : opts) {
opt_ptrs.push_back(opt.c_str());
}
this->traits.set_options(opt_ptrs);
}

View File

@@ -4,13 +4,14 @@
#include "esphome/core/time.h"
#include "esphome/components/spi/spi.h"
#include "esphome/components/display/display.h"
namespace esphome {
namespace max7219 {
class MAX7219Component;
using max7219_writer_t = std::function<void(MAX7219Component &)>;
using max7219_writer_t = display::DisplayWriter<MAX7219Component>;
class MAX7219Component : public PollingComponent,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
@@ -57,7 +58,7 @@ class MAX7219Component : public PollingComponent,
uint8_t num_chips_{1};
uint8_t *buffer_;
bool reverse_{false};
optional<max7219_writer_t> writer_{};
max7219_writer_t writer_{};
};
} // namespace max7219

View File

@@ -271,7 +271,11 @@ void MAX7219Component::send64pixels(uint8_t chip, const uint8_t pixels[8]) {
}
}
} else if (this->orientation_ == 1) {
b = pixels[col];
if (this->flip_x_) {
b = pixels[7 - col];
} else {
b = pixels[col];
}
} else if (this->orientation_ == 2) {
for (uint8_t i = 0; i < 8; i++) {
if (this->flip_x_) {
@@ -282,7 +286,11 @@ void MAX7219Component::send64pixels(uint8_t chip, const uint8_t pixels[8]) {
}
} else {
for (uint8_t i = 0; i < 8; i++) {
b |= ((pixels[7 - col] >> i) & 1) << (7 - i);
if (this->flip_x_) {
b |= ((pixels[col] >> i) & 1) << (7 - i);
} else {
b |= ((pixels[7 - col] >> i) & 1) << (7 - i);
}
}
}
// send this byte to display at selected chip

View File

@@ -23,7 +23,7 @@ enum ScrollMode {
class MAX7219Component;
using max7219_writer_t = std::function<void(MAX7219Component &)>;
using max7219_writer_t = display::DisplayWriter<MAX7219Component>;
class MAX7219Component : public display::DisplayBuffer,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
@@ -117,7 +117,7 @@ class MAX7219Component : public display::DisplayBuffer,
uint32_t last_scroll_ = 0;
uint16_t stepsleft_;
size_t get_buffer_length_();
optional<max7219_writer_t> writer_local_{};
max7219_writer_t writer_local_{};
};
} // namespace max7219digit

View File

@@ -56,7 +56,7 @@ void MCP23016::pin_mode(uint8_t pin, gpio::Flags flags) {
this->update_reg_(pin, false, iodir);
}
}
float MCP23016::get_setup_priority() const { return setup_priority::IO; }
float MCP23016::get_setup_priority() const { return setup_priority::HARDWARE; }
bool MCP23016::read_reg_(uint8_t reg, uint8_t *value) {
if (this->is_failed())
return false;

View File

@@ -3,6 +3,7 @@ import binascii
from esphome import automation
import esphome.codegen as cg
from esphome.components import modbus
from esphome.components.const import CONF_ENABLED
import esphome.config_validation as cv
from esphome.const import (
CONF_ADDRESS,
@@ -20,7 +21,6 @@ from .const import (
CONF_BYTE_OFFSET,
CONF_COMMAND_THROTTLE,
CONF_CUSTOM_COMMAND,
CONF_ENABLED,
CONF_FORCE_NEW_RANGE,
CONF_MAX_CMD_RETRIES,
CONF_MODBUS_CONTROLLER_ID,

View File

@@ -2,7 +2,6 @@ CONF_ALLOW_DUPLICATE_COMMANDS = "allow_duplicate_commands"
CONF_BITMASK = "bitmask"
CONF_BYTE_OFFSET = "byte_offset"
CONF_COMMAND_THROTTLE = "command_throttle"
CONF_ENABLED = "enabled"
CONF_OFFLINE_SKIP_UPDATES = "offline_skip_updates"
CONF_CUSTOM_COMMAND = "custom_command"
CONF_FORCE_NEW_RANGE = "force_new_range"

View File

@@ -36,7 +36,7 @@ void MQTTAlarmControlPanelComponent::setup() {
} else if (strcasecmp(payload.c_str(), "TRIGGERED") == 0) {
call.triggered();
} else {
ESP_LOGW(TAG, "'%s': Received unknown command payload %s", this->friendly_name().c_str(), payload.c_str());
ESP_LOGW(TAG, "'%s': Received unknown command payload %s", this->friendly_name_().c_str(), payload.c_str());
}
call.perform();
});

View File

@@ -31,8 +31,9 @@ MQTTBinarySensorComponent::MQTTBinarySensorComponent(binary_sensor::BinarySensor
void MQTTBinarySensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
if (!this->binary_sensor_->get_device_class().empty())
root[MQTT_DEVICE_CLASS] = this->binary_sensor_->get_device_class();
const auto device_class = this->binary_sensor_->get_device_class_ref();
if (!device_class.empty())
root[MQTT_DEVICE_CLASS] = device_class;
if (this->binary_sensor_->is_status_binary_sensor())
root[MQTT_PAYLOAD_ON] = mqtt::global_mqtt_client->get_availability().payload_available;
if (this->binary_sensor_->is_status_binary_sensor())

View File

@@ -20,7 +20,7 @@ void MQTTButtonComponent::setup() {
if (payload == "PRESS") {
this->button_->press();
} else {
ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name().c_str(), payload.c_str());
ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name_().c_str(), payload.c_str());
this->status_momentary_warning("state", 5000);
}
});
@@ -33,8 +33,9 @@ void MQTTButtonComponent::dump_config() {
void MQTTButtonComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
config.state_topic = false;
if (!this->button_->get_device_class().empty()) {
root[MQTT_DEVICE_CLASS] = this->button_->get_device_class();
const auto device_class = this->button_->get_device_class_ref();
if (!device_class.empty()) {
root[MQTT_DEVICE_CLASS] = device_class;
}
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
}

View File

@@ -64,11 +64,11 @@ bool MQTTComponent::send_discovery_() {
const MQTTDiscoveryInfo &discovery_info = global_mqtt_client->get_discovery_info();
if (discovery_info.clean) {
ESP_LOGV(TAG, "'%s': Cleaning discovery", this->friendly_name().c_str());
ESP_LOGV(TAG, "'%s': Cleaning discovery", this->friendly_name_().c_str());
return global_mqtt_client->publish(this->get_discovery_topic_(discovery_info), "", 0, this->qos_, true);
}
ESP_LOGV(TAG, "'%s': Sending discovery", this->friendly_name().c_str());
ESP_LOGV(TAG, "'%s': Sending discovery", this->friendly_name_().c_str());
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
return global_mqtt_client->publish_json(
@@ -85,12 +85,13 @@ bool MQTTComponent::send_discovery_() {
}
// Fields from EntityBase
root[MQTT_NAME] = this->get_entity()->has_own_name() ? this->friendly_name() : "";
root[MQTT_NAME] = this->get_entity()->has_own_name() ? this->friendly_name_() : "";
if (this->is_disabled_by_default())
if (this->is_disabled_by_default_())
root[MQTT_ENABLED_BY_DEFAULT] = false;
if (!this->get_icon().empty())
root[MQTT_ICON] = this->get_icon();
const auto icon_ref = this->get_icon_ref_();
if (!icon_ref.empty())
root[MQTT_ICON] = icon_ref;
const auto entity_category = this->get_entity()->get_entity_category();
switch (entity_category) {
@@ -122,7 +123,7 @@ bool MQTTComponent::send_discovery_() {
const MQTTDiscoveryInfo &discovery_info = global_mqtt_client->get_discovery_info();
if (discovery_info.unique_id_generator == MQTT_MAC_ADDRESS_UNIQUE_ID_GENERATOR) {
char friendly_name_hash[9];
sprintf(friendly_name_hash, "%08" PRIx32, fnv1_hash(this->friendly_name()));
sprintf(friendly_name_hash, "%08" PRIx32, fnv1_hash(this->friendly_name_()));
friendly_name_hash[8] = 0; // ensure the hash-string ends with null
root[MQTT_UNIQUE_ID] = get_mac_address() + "-" + this->component_type() + "-" + friendly_name_hash;
} else {
@@ -184,7 +185,7 @@ bool MQTTComponent::is_discovery_enabled() const {
}
std::string MQTTComponent::get_default_object_id_() const {
return str_sanitize(str_snake_case(this->friendly_name()));
return str_sanitize(str_snake_case(this->friendly_name_()));
}
void MQTTComponent::subscribe(const std::string &topic, mqtt_callback_t callback, uint8_t qos) {
@@ -268,9 +269,9 @@ void MQTTComponent::schedule_resend_state() { this->resend_state_ = true; }
bool MQTTComponent::is_connected_() const { return global_mqtt_client->is_connected(); }
// Pull these properties from EntityBase if not overridden
std::string MQTTComponent::friendly_name() const { return this->get_entity()->get_name(); }
std::string MQTTComponent::get_icon() const { return this->get_entity()->get_icon(); }
bool MQTTComponent::is_disabled_by_default() const { return this->get_entity()->is_disabled_by_default(); }
std::string MQTTComponent::friendly_name_() const { return this->get_entity()->get_name(); }
StringRef MQTTComponent::get_icon_ref_() const { return this->get_entity()->get_icon_ref(); }
bool MQTTComponent::is_disabled_by_default_() const { return this->get_entity()->is_disabled_by_default(); }
bool MQTTComponent::is_internal() {
if (this->has_custom_state_topic_) {
// If the custom state_topic is null, return true as it is internal and should not publish

View File

@@ -165,13 +165,13 @@ class MQTTComponent : public Component {
virtual const EntityBase *get_entity() const = 0;
/// Get the friendly name of this MQTT component.
virtual std::string friendly_name() const;
std::string friendly_name_() const;
/// Get the icon field of this component
virtual std::string get_icon() const;
/// Get the icon field of this component as StringRef
StringRef get_icon_ref_() const;
/// Get whether the underlying Entity is disabled by default
virtual bool is_disabled_by_default() const;
bool is_disabled_by_default_() const;
/// Get the MQTT topic that new states will be shared to.
std::string get_state_topic_() const;

View File

@@ -68,8 +68,9 @@ void MQTTCoverComponent::dump_config() {
}
void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
if (!this->cover_->get_device_class().empty())
root[MQTT_DEVICE_CLASS] = this->cover_->get_device_class();
const auto device_class = this->cover_->get_device_class_ref();
if (!device_class.empty())
root[MQTT_DEVICE_CLASS] = device_class;
auto traits = this->cover_->get_traits();
if (traits.get_is_assumed_state()) {

View File

@@ -21,8 +21,9 @@ void MQTTEventComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConf
for (const auto &event_type : this->event_->get_event_types())
event_types.add(event_type);
if (!this->event_->get_device_class().empty())
root[MQTT_DEVICE_CLASS] = this->event_->get_device_class();
const auto device_class = this->event_->get_device_class_ref();
if (!device_class.empty())
root[MQTT_DEVICE_CLASS] = device_class;
config.command_topic = false;
}

View File

@@ -24,15 +24,15 @@ void MQTTFanComponent::setup() {
auto val = parse_on_off(payload.c_str());
switch (val) {
case PARSE_ON:
ESP_LOGD(TAG, "'%s' Turning Fan ON.", this->friendly_name().c_str());
ESP_LOGD(TAG, "'%s' Turning Fan ON.", this->friendly_name_().c_str());
this->state_->turn_on().perform();
break;
case PARSE_OFF:
ESP_LOGD(TAG, "'%s' Turning Fan OFF.", this->friendly_name().c_str());
ESP_LOGD(TAG, "'%s' Turning Fan OFF.", this->friendly_name_().c_str());
this->state_->turn_off().perform();
break;
case PARSE_TOGGLE:
ESP_LOGD(TAG, "'%s' Toggling Fan.", this->friendly_name().c_str());
ESP_LOGD(TAG, "'%s' Toggling Fan.", this->friendly_name_().c_str());
this->state_->toggle().perform();
break;
case PARSE_NONE:
@@ -48,11 +48,11 @@ void MQTTFanComponent::setup() {
auto val = parse_on_off(payload.c_str(), "forward", "reverse");
switch (val) {
case PARSE_ON:
ESP_LOGD(TAG, "'%s': Setting direction FORWARD", this->friendly_name().c_str());
ESP_LOGD(TAG, "'%s': Setting direction FORWARD", this->friendly_name_().c_str());
this->state_->make_call().set_direction(fan::FanDirection::FORWARD).perform();
break;
case PARSE_OFF:
ESP_LOGD(TAG, "'%s': Setting direction REVERSE", this->friendly_name().c_str());
ESP_LOGD(TAG, "'%s': Setting direction REVERSE", this->friendly_name_().c_str());
this->state_->make_call().set_direction(fan::FanDirection::REVERSE).perform();
break;
case PARSE_TOGGLE:
@@ -75,11 +75,11 @@ void MQTTFanComponent::setup() {
auto val = parse_on_off(payload.c_str(), "oscillate_on", "oscillate_off");
switch (val) {
case PARSE_ON:
ESP_LOGD(TAG, "'%s': Setting oscillating ON", this->friendly_name().c_str());
ESP_LOGD(TAG, "'%s': Setting oscillating ON", this->friendly_name_().c_str());
this->state_->make_call().set_oscillating(true).perform();
break;
case PARSE_OFF:
ESP_LOGD(TAG, "'%s': Setting oscillating OFF", this->friendly_name().c_str());
ESP_LOGD(TAG, "'%s': Setting oscillating OFF", this->friendly_name_().c_str());
this->state_->make_call().set_oscillating(false).perform();
break;
case PARSE_TOGGLE:

View File

@@ -24,7 +24,7 @@ void MQTTLockComponent::setup() {
} else if (strcasecmp(payload.c_str(), "OPEN") == 0) {
this->lock_->open();
} else {
ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name().c_str(), payload.c_str());
ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name_().c_str(), payload.c_str());
this->status_momentary_warning("state", 5000);
}
});

View File

@@ -44,8 +44,9 @@ void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon
root[MQTT_MIN] = traits.get_min_value();
root[MQTT_MAX] = traits.get_max_value();
root[MQTT_STEP] = traits.get_step();
if (!this->number_->traits.get_unit_of_measurement().empty())
root[MQTT_UNIT_OF_MEASUREMENT] = this->number_->traits.get_unit_of_measurement();
const auto unit_of_measurement = this->number_->traits.get_unit_of_measurement_ref();
if (!unit_of_measurement.empty())
root[MQTT_UNIT_OF_MEASUREMENT] = unit_of_measurement;
switch (this->number_->traits.get_mode()) {
case NUMBER_MODE_AUTO:
break;
@@ -56,8 +57,9 @@ void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon
root[MQTT_MODE] = "slider";
break;
}
if (!this->number_->traits.get_device_class().empty())
root[MQTT_DEVICE_CLASS] = this->number_->traits.get_device_class();
const auto device_class = this->number_->traits.get_device_class_ref();
if (!device_class.empty())
root[MQTT_DEVICE_CLASS] = device_class;
config.command_topic = true;
}

View File

@@ -45,12 +45,14 @@ void MQTTSensorComponent::disable_expire_after() { this->expire_after_ = 0; }
void MQTTSensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
if (!this->sensor_->get_device_class().empty()) {
root[MQTT_DEVICE_CLASS] = this->sensor_->get_device_class();
const auto device_class = this->sensor_->get_device_class_ref();
if (!device_class.empty()) {
root[MQTT_DEVICE_CLASS] = device_class;
}
if (!this->sensor_->get_unit_of_measurement().empty())
root[MQTT_UNIT_OF_MEASUREMENT] = this->sensor_->get_unit_of_measurement();
const auto unit_of_measurement = this->sensor_->get_unit_of_measurement_ref();
if (!unit_of_measurement.empty())
root[MQTT_UNIT_OF_MEASUREMENT] = unit_of_measurement;
if (this->get_expire_after() > 0)
root[MQTT_EXPIRE_AFTER] = this->get_expire_after() / 1000;

View File

@@ -29,7 +29,7 @@ void MQTTSwitchComponent::setup() {
break;
case PARSE_NONE:
default:
ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name().c_str(), payload.c_str());
ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name_().c_str(), payload.c_str());
this->status_momentary_warning("state", 5000);
break;
}

View File

@@ -16,8 +16,9 @@ using namespace esphome::text_sensor;
MQTTTextSensor::MQTTTextSensor(TextSensor *sensor) : sensor_(sensor) {}
void MQTTTextSensor::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
if (!this->sensor_->get_device_class().empty()) {
root[MQTT_DEVICE_CLASS] = this->sensor_->get_device_class();
const auto device_class = this->sensor_->get_device_class_ref();
if (!device_class.empty()) {
root[MQTT_DEVICE_CLASS] = device_class;
}
config.command_topic = false;
}

View File

@@ -20,7 +20,7 @@ void MQTTUpdateComponent::setup() {
if (payload == "INSTALL") {
this->update_->perform();
} else {
ESP_LOGW(TAG, "'%s': Received unknown update payload: %s", this->friendly_name().c_str(), payload.c_str());
ESP_LOGW(TAG, "'%s': Received unknown update payload: %s", this->friendly_name_().c_str(), payload.c_str());
this->status_momentary_warning("state", 5000);
}
});

View File

@@ -50,8 +50,9 @@ void MQTTValveComponent::dump_config() {
}
void MQTTValveComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
if (!this->valve_->get_device_class().empty()) {
root[MQTT_DEVICE_CLASS] = this->valve_->get_device_class();
const auto device_class = this->valve_->get_device_class_ref();
if (!device_class.empty()) {
root[MQTT_DEVICE_CLASS] = device_class;
}
auto traits = this->valve_->get_traits();

View File

@@ -9,6 +9,7 @@
#include "esphome/components/uart/uart.h"
#include "nextion_base.h"
#include "nextion_component.h"
#include "esphome/components/display/display.h"
#include "esphome/components/display/display_color_utils.h"
#ifdef USE_NEXTION_TFT_UPLOAD
@@ -31,7 +32,7 @@ namespace nextion {
class Nextion;
class NextionComponentBase;
using nextion_writer_t = std::function<void(Nextion &)>;
using nextion_writer_t = display::DisplayWriter<Nextion>;
static const std::string COMMAND_DELIMITER{static_cast<char>(255), static_cast<char>(255), static_cast<char>(255)};
@@ -1471,7 +1472,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
CallbackManager<void(uint8_t, uint8_t, bool)> touch_callback_{};
CallbackManager<void()> buffer_overflow_callback_{};
optional<nextion_writer_t> writer_;
nextion_writer_t writer_;
optional<float> brightness_;
#ifdef USE_NEXTION_CONFIG_DUMP_DEVICE_INFO

View File

@@ -3,6 +3,7 @@
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/components/ble_client/ble_client.h"
#include "esphome/components/display/display.h"
#include <cinttypes>
@@ -29,7 +30,7 @@ enum UNIT {
UNIT_DEG_E, ///< show "°E"
};
using pvvx_writer_t = std::function<void(PVVXDisplay &)>;
using pvvx_writer_t = display::DisplayWriter<PVVXDisplay>;
class PVVXDisplay : public ble_client::BLEClientNode, public PollingComponent {
public:
@@ -126,7 +127,7 @@ class PVVXDisplay : public ble_client::BLEClientNode, public PollingComponent {
esp32_ble_tracker::ESPBTUUID char_uuid_ =
esp32_ble_tracker::ESPBTUUID::from_raw("00001f1f-0000-1000-8000-00805f9b34fb");
optional<pvvx_writer_t> writer_{};
pvvx_writer_t writer_{};
};
} // namespace pvvx_mithermometer

View File

@@ -35,9 +35,9 @@ void Rtttl::dump_config() {
void Rtttl::play(std::string rtttl) {
if (this->state_ != State::STATE_STOPPED && this->state_ != State::STATE_STOPPING) {
int pos = this->rtttl_.find(':');
auto name = this->rtttl_.substr(0, pos);
ESP_LOGW(TAG, "Already playing: %s", name.c_str());
size_t pos = this->rtttl_.find(':');
size_t len = (pos != std::string::npos) ? pos : this->rtttl_.length();
ESP_LOGW(TAG, "Already playing: %.*s", (int) len, this->rtttl_.c_str());
return;
}
@@ -59,8 +59,7 @@ void Rtttl::play(std::string rtttl) {
return;
}
auto name = this->rtttl_.substr(0, this->position_);
ESP_LOGD(TAG, "Playing song %s", name.c_str());
ESP_LOGD(TAG, "Playing song %.*s", (int) this->position_, this->rtttl_.c_str());
// get default duration
this->position_ = this->rtttl_.find("d=", this->position_);

View File

@@ -35,7 +35,7 @@ void Select::publish_state(size_t index) {
this->state_callback_.call(std::string(option), index);
}
const char *Select::current_option() const { return this->option_at(this->active_index_); }
const char *Select::current_option() const { return this->has_state() ? this->option_at(this->active_index_) : ""; }
void Select::add_on_state_callback(std::function<void(std::string, size_t)> &&callback) {
this->state_callback_.add(std::move(callback));

View File

@@ -77,35 +77,32 @@ class Select : public EntityBase {
void add_on_state_callback(std::function<void(std::string, size_t)> &&callback);
/** Set the value of the select by index, this is an optional virtual method.
*
* This method is called by the SelectCall when the index is already known.
* Default implementation converts to string and calls control().
* Override this to work directly with indices and avoid string conversions.
*
* @param index The index as validated by the SelectCall.
*/
virtual void control(size_t index) { this->control(this->option_at(index)); }
protected:
friend class SelectCall;
size_t active_index_{0};
/** Set the value of the select by index, this is an optional virtual method.
*
* IMPORTANT: At least ONE of the two control() methods must be overridden by derived classes.
* Overriding this index-based version is PREFERRED as it avoids string conversions.
*
* This method is called by the SelectCall when the index is already known.
* Default implementation converts to string and calls control(const std::string&).
*
* @param index The index as validated by the SelectCall.
*/
virtual void control(size_t index) { this->control(this->option_at(index)); }
/** Set the value of the select, this is a virtual method that each select integration can implement.
*
* This method is called by control(size_t) when not overridden, or directly by external code.
* Integrations can either:
* 1. Override this method to handle string-based control (traditional approach)
* 2. Override control(size_t) instead to work with indices directly (recommended)
* IMPORTANT: At least ONE of the two control() methods must be overridden by derived classes.
* Overriding control(size_t) is PREFERRED as it avoids string conversions.
*
* This method is called by control(size_t) when not overridden, or directly by external code.
* Default implementation converts to index and calls control(size_t).
*
* Delegation chain:
* - SelectCall::perform() → control(size_t) → [if not overridden] → control(string)
* - External code → control(string) → publish_state(string) → publish_state(size_t)
*
* @param value The value as validated by the SelectCall.
* @param value The value as validated by the caller.
*/
virtual void control(const std::string &value) {
auto index = this->index_of(value);

View File

@@ -7,8 +7,8 @@ void SelectTraits::set_options(const std::initializer_list<const char *> &option
void SelectTraits::set_options(const FixedVector<const char *> &options) {
this->options_.init(options.size());
for (size_t i = 0; i < options.size(); i++) {
this->options_[i] = options[i];
for (const auto &opt : options) {
this->options_.push_back(opt);
}
}

View File

@@ -78,7 +78,7 @@ void SGP30Component::setup() {
uint32_t hash = fnv1_hash(App.get_compilation_time() + std::to_string(this->serial_number_));
this->pref_ = global_preferences->make_preference<SGP30Baselines>(hash, true);
if (this->pref_.load(&this->baselines_storage_)) {
if (this->store_baseline_ && this->pref_.load(&this->baselines_storage_)) {
ESP_LOGI(TAG, "Loaded eCO2 baseline: 0x%04X, TVOC baseline: 0x%04X", this->baselines_storage_.eco2,
baselines_storage_.tvoc);
this->eco2_baseline_ = this->baselines_storage_.eco2;

View File

@@ -9,7 +9,7 @@ namespace st7920 {
class ST7920;
using st7920_writer_t = std::function<void(ST7920 &)>;
using st7920_writer_t = display::DisplayWriter<ST7920>;
class ST7920 : public display::DisplayBuffer,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH, spi::CLOCK_PHASE_TRAILING,
@@ -44,7 +44,7 @@ class ST7920 : public display::DisplayBuffer,
void end_transaction_();
int16_t width_ = 128, height_ = 64;
optional<st7920_writer_t> writer_local_{};
st7920_writer_t writer_local_{};
};
} // namespace st7920

View File

@@ -3,13 +3,14 @@
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/components/display/display.h"
namespace esphome {
namespace tm1621 {
class TM1621Display;
using tm1621_writer_t = std::function<void(TM1621Display &)>;
using tm1621_writer_t = display::DisplayWriter<TM1621Display>;
class TM1621Display : public PollingComponent {
public:
@@ -59,7 +60,7 @@ class TM1621Display : public PollingComponent {
GPIOPin *cs_pin_;
GPIOPin *read_pin_;
GPIOPin *write_pin_;
optional<tm1621_writer_t> writer_{};
tm1621_writer_t writer_{};
char row_[2][12];
uint8_t state_;
uint8_t device_;

View File

@@ -4,6 +4,7 @@
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/core/time.h"
#include "esphome/components/display/display.h"
#include <vector>
@@ -19,7 +20,7 @@ class TM1637Display;
class TM1637Key;
#endif
using tm1637_writer_t = std::function<void(TM1637Display &)>;
using tm1637_writer_t = display::DisplayWriter<TM1637Display>;
class TM1637Display : public PollingComponent {
public:
@@ -78,7 +79,7 @@ class TM1637Display : public PollingComponent {
uint8_t length_;
bool inverted_;
bool on_{true};
optional<tm1637_writer_t> writer_{};
tm1637_writer_t writer_{};
uint8_t buffer_[6] = {0};
#ifdef USE_BINARY_SENSOR
std::vector<TM1637Key *> tm1637_keys_{};

View File

@@ -5,6 +5,7 @@
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/core/time.h"
#include "esphome/components/display/display.h"
#include <vector>
@@ -18,7 +19,7 @@ class KeyListener {
class TM1638Component;
using tm1638_writer_t = std::function<void(TM1638Component &)>;
using tm1638_writer_t = display::DisplayWriter<TM1638Component>;
class TM1638Component : public PollingComponent {
public:
@@ -70,7 +71,7 @@ class TM1638Component : public PollingComponent {
GPIOPin *stb_pin_;
GPIOPin *dio_pin_;
uint8_t *buffer_ = new uint8_t[8];
optional<tm1638_writer_t> writer_{};
tm1638_writer_t writer_{};
std::vector<KeyListener *> listeners_{};
};

View File

@@ -657,7 +657,8 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
ESP_LOGW(TAG, "No text in STT_END event");
return;
} else if (text.length() > 500) {
text = text.substr(0, 497) + "...";
text.resize(497);
text += "...";
}
ESP_LOGD(TAG, "Speech recognised as: \"%s\"", text.c_str());
this->defer([this, text]() { this->stt_end_trigger_->trigger(text); });
@@ -714,7 +715,8 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
return;
}
if (text.length() > 500) {
text = text.substr(0, 497) + "...";
text.resize(497);
text += "...";
}
ESP_LOGD(TAG, "Response: \"%s\"", text.c_str());
this->defer([this, text]() {

View File

@@ -353,9 +353,8 @@ void AsyncWebServerResponse::addHeader(const char *name, const char *value) {
void AsyncResponseStream::print(float value) {
// Use stack buffer to avoid temporary string allocation
// Size: sign (1) + digits (10) + decimal (1) + precision (6) + exponent (5) + null (1) = 24, use 32 for safety
constexpr size_t float_buf_size = 32;
char buf[float_buf_size];
int len = snprintf(buf, float_buf_size, "%f", value);
char buf[32];
int len = snprintf(buf, sizeof(buf), "%f", value);
this->content_.append(buf, len);
}

View File

@@ -10,6 +10,8 @@
namespace esphome {
namespace wifi_info {
static constexpr size_t MAX_STATE_LENGTH = 255;
class IPAddressWiFiInfo : public PollingComponent, public text_sensor::TextSensor {
public:
void update() override {
@@ -71,11 +73,14 @@ class ScanResultsWiFiInfo : public PollingComponent, public text_sensor::TextSen
scan_results += "dB\n";
}
// There's a limit of 255 characters per state.
// Longer states just don't get sent so we truncate it.
if (scan_results.length() > MAX_STATE_LENGTH) {
scan_results.resize(MAX_STATE_LENGTH);
}
if (this->last_scan_results_ != scan_results) {
this->last_scan_results_ = scan_results;
// There's a limit of 255 characters per state.
// Longer states just don't get sent so we truncate it.
this->publish_state(scan_results.substr(0, 255));
this->publish_state(scan_results);
}
}
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }

View File

@@ -1,5 +1,6 @@
import esphome.codegen as cg
from esphome.components import binary_sensor
from esphome.components.const import CONF_ENABLED
import esphome.config_validation as cv
from esphome.const import (
CONF_STATUS,
@@ -9,8 +10,6 @@ from esphome.const import (
from . import CONF_WIREGUARD_ID, Wireguard
CONF_ENABLED = "enabled"
DEPENDENCIES = ["wireguard"]
CONFIG_SCHEMA = {

View File

@@ -707,15 +707,6 @@ class EsphomeCore:
def relative_piolibdeps_path(self, *path: str | Path) -> Path:
return self.relative_build_path(".piolibdeps", *path)
@property
def platformio_cache_dir(self) -> str:
"""Get the PlatformIO cache directory path."""
# Check if running in Docker/HA addon with custom cache dir
if (cache_dir := os.environ.get("PLATFORMIO_CACHE_DIR")) and cache_dir.strip():
return cache_dir
# Default PlatformIO cache location
return os.path.expanduser("~/.platformio/.cache")
@property
def firmware_bin(self) -> Path:
if self.is_libretiny:

View File

@@ -414,8 +414,10 @@ int8_t step_to_accuracy_decimals(float step) {
return str.length() - dot_pos - 1;
}
// Store BASE64 characters as array - automatically placed in flash/ROM on embedded platforms
static const char BASE64_CHARS[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
// Use C-style string constant to store in ROM instead of RAM (saves 24 bytes)
static constexpr const char *BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789+/";
// Helper function to find the index of a base64 character in the lookup table.
// Returns the character's position (0-63) if found, or 0 if not found.
@@ -425,8 +427,8 @@ static const char BASE64_CHARS[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr
// stops processing at the first invalid character due to the is_base64() check in its
// while loop condition, making this edge case harmless in practice.
static inline uint8_t base64_find_char(char c) {
const void *ptr = memchr(BASE64_CHARS, c, sizeof(BASE64_CHARS));
return ptr ? (static_cast<const char *>(ptr) - BASE64_CHARS) : 0;
const char *pos = strchr(BASE64_CHARS, c);
return pos ? (pos - BASE64_CHARS) : 0;
}
static inline bool is_base64(char c) { return (isalnum(c) || (c == '+') || (c == '/')); }

View File

@@ -144,9 +144,6 @@ template<typename T, size_t N> class StaticVector {
size_t size() const { return count_; }
bool empty() const { return count_ == 0; }
// Direct access to size counter for efficient in-place construction
size_t &count() { return count_; }
T &operator[](size_t i) { return data_[i]; }
const T &operator[](size_t i) const { return data_[i]; }
@@ -251,6 +248,8 @@ template<typename T> class FixedVector {
}
// Allocate capacity - can be called multiple times to reinit
// IMPORTANT: After calling init(), you MUST use push_back() to add elements.
// Direct assignment via operator[] does NOT update the size counter.
void init(size_t n) {
cleanup_();
reset_();

View File

@@ -94,9 +94,10 @@ class Scheduler {
} name_;
uint32_t interval;
// Split time to handle millis() rollover. The scheduler combines the 32-bit millis()
// with a 16-bit rollover counter to create a 48-bit time space (stored as 64-bit
// for compatibility). With 49.7 days per 32-bit rollover, the 16-bit counter
// supports 49.7 days × 65536 = ~8900 years. This ensures correct scheduling
// with a 16-bit rollover counter to create a 48-bit time space (using 32+16 bits).
// This is intentionally limited to 48 bits, not stored as a full 64-bit value.
// With 49.7 days per 32-bit rollover, the 16-bit counter supports
// 49.7 days × 65536 = ~8900 years. This ensures correct scheduling
// even when devices run for months. Split into two fields for better memory
// alignment on 32-bit systems.
uint32_t next_execution_low_; // Lower 32 bits of execution time (millis value)

View File

@@ -145,16 +145,7 @@ def run_compile(config, verbose):
args = []
if CONF_COMPILE_PROCESS_LIMIT in config[CONF_ESPHOME]:
args += [f"-j{config[CONF_ESPHOME][CONF_COMPILE_PROCESS_LIMIT]}"]
result = run_platformio_cli_run(config, verbose, *args)
# Run memory analysis if enabled
if config.get(CONF_ESPHOME, {}).get("analyze_memory", False):
try:
analyze_memory_usage(config)
except Exception as e:
_LOGGER.warning("Failed to analyze memory usage: %s", e)
return result
return run_platformio_cli_run(config, verbose, *args)
def _run_idedata(config):
@@ -403,74 +394,3 @@ class IDEData:
if path.endswith(".exe")
else f"{path[:-3]}readelf"
)
def analyze_memory_usage(config: dict[str, Any]) -> None:
"""Analyze memory usage by component after compilation."""
# Lazy import to avoid overhead when not needed
from esphome.analyze_memory.cli import MemoryAnalyzerCLI
from esphome.analyze_memory.helpers import get_esphome_components
idedata = get_idedata(config)
# Get paths to tools
elf_path = idedata.firmware_elf_path
objdump_path = idedata.objdump_path
readelf_path = idedata.readelf_path
# Debug logging
_LOGGER.debug("ELF path from idedata: %s", elf_path)
# Check if file exists
if not Path(elf_path).exists():
# Try alternate path
alt_path = Path(CORE.relative_build_path(".pioenvs", CORE.name, "firmware.elf"))
if alt_path.exists():
elf_path = str(alt_path)
_LOGGER.debug("Using alternate ELF path: %s", elf_path)
else:
_LOGGER.warning("ELF file not found at %s or %s", elf_path, alt_path)
return
# Extract external components from config
external_components = set()
# Get the list of built-in ESPHome components
builtin_components = get_esphome_components()
# Special non-component keys that appear in configs
NON_COMPONENT_KEYS = {
CONF_ESPHOME,
"substitutions",
"packages",
"globals",
"<<",
}
# Check all top-level keys in config
for key in config:
if key not in builtin_components and key not in NON_COMPONENT_KEYS:
# This is an external component
external_components.add(key)
_LOGGER.debug("Detected external components: %s", external_components)
# Create analyzer and run analysis
analyzer = MemoryAnalyzerCLI(
elf_path, objdump_path, readelf_path, external_components
)
analyzer.analyze()
# Generate and print report
report = analyzer.generate_report()
_LOGGER.info("\n%s", report)
# Optionally save to file
if config.get(CONF_ESPHOME, {}).get("memory_report_file"):
report_file = Path(config[CONF_ESPHOME]["memory_report_file"])
if report_file.suffix == ".json":
report_file.write_text(analyzer.to_json())
_LOGGER.info("Memory report saved to %s", report_file)
else:
report_file.write_text(report)
_LOGGER.info("Memory report saved to %s", report_file)

View File

@@ -66,6 +66,5 @@ def test_text_config_lamda_is_set(generate_main):
main_cpp = generate_main("tests/component_tests/text/test_text.yaml")
# Then
# Stateless lambda optimization: empty capture list allows function pointer conversion
assert "it_4->set_template([]() -> esphome::optional<std::string> {" in main_cpp
assert 'return std::string{"Hello"};' in main_cpp

View File

@@ -4,6 +4,11 @@ gp8403:
voltage: 5V
- id: gp8403_10v
i2c_id: i2c_bus
model: GP8403
voltage: 10V
- id: gp8413
i2c_id: i2c_bus
model: GP8413
voltage: 10V
output:
@@ -15,3 +20,7 @@ output:
gp8403_id: gp8403_10v
id: gp8403_output_1
channel: 1
- platform: gp8403
gp8403_id: gp8413
id: gp8413_output_2
channel: 1

View File

@@ -1,5 +1,6 @@
display:
- platform: sdl
id: image_display
auto_clear_enabled: false
dimensions:
width: 480

View File

@@ -191,7 +191,7 @@ lvgl:
args: ['lv_event_code_name_for(event->code).c_str()']
skip: true
layout:
type: flex
type: Flex
pad_row: 4
pad_column: 4px
flex_align_main: center
@@ -863,7 +863,7 @@ lvgl:
width: 100%
pad_all: 8
layout:
type: grid
type: GRid
grid_row_align: end
grid_rows: [25px, fr(1), content]
grid_columns: [40, fr(1), fr(1)]
@@ -1014,7 +1014,7 @@ lvgl:
r_mod: -20
opa: 0%
- id: page3
layout: horizontal
layout: Horizontal
widgets:
- keyboard:
id: lv_keyboard

View File

@@ -13,11 +13,14 @@ display:
binary_sensor:
- platform: sdl
sdl_id: sdl_display
id: key_up
key: SDLK_UP
- platform: sdl
sdl_id: sdl_display
id: key_down
key: SDLK_DOWN
- platform: sdl
sdl_id: sdl_display
id: key_enter
key: SDLK_RETURN

View File

@@ -670,45 +670,3 @@ class TestEsphomeCore:
os.environ.pop("ESPHOME_IS_HA_ADDON", None)
os.environ.pop("ESPHOME_DATA_DIR", None)
assert target.data_dir == Path(expected_default)
def test_platformio_cache_dir_with_env_var(self):
"""Test platformio_cache_dir when PLATFORMIO_CACHE_DIR env var is set."""
target = core.EsphomeCore()
test_cache_dir = "/custom/cache/dir"
with patch.dict(os.environ, {"PLATFORMIO_CACHE_DIR": test_cache_dir}):
assert target.platformio_cache_dir == test_cache_dir
def test_platformio_cache_dir_without_env_var(self):
"""Test platformio_cache_dir defaults to ~/.platformio/.cache."""
target = core.EsphomeCore()
with patch.dict(os.environ, {}, clear=True):
# Ensure env var is not set
os.environ.pop("PLATFORMIO_CACHE_DIR", None)
expected = os.path.expanduser("~/.platformio/.cache")
assert target.platformio_cache_dir == expected
def test_platformio_cache_dir_empty_env_var(self):
"""Test platformio_cache_dir with empty env var falls back to default."""
target = core.EsphomeCore()
with patch.dict(os.environ, {"PLATFORMIO_CACHE_DIR": ""}):
expected = os.path.expanduser("~/.platformio/.cache")
assert target.platformio_cache_dir == expected
def test_platformio_cache_dir_whitespace_env_var(self):
"""Test platformio_cache_dir with whitespace-only env var falls back to default."""
target = core.EsphomeCore()
with patch.dict(os.environ, {"PLATFORMIO_CACHE_DIR": " "}):
expected = os.path.expanduser("~/.platformio/.cache")
assert target.platformio_cache_dir == expected
def test_platformio_cache_dir_docker_addon_path(self):
"""Test platformio_cache_dir in Docker/HA addon environment."""
target = core.EsphomeCore()
addon_cache = "/data/cache/platformio"
with patch.dict(os.environ, {"PLATFORMIO_CACHE_DIR": addon_cache}):
assert target.platformio_cache_dir == addon_cache

View File

@@ -355,7 +355,6 @@ def test_clean_build(
mock_core.relative_pioenvs_path.return_value = pioenvs_dir
mock_core.relative_piolibdeps_path.return_value = piolibdeps_dir
mock_core.relative_build_path.return_value = dependencies_lock
mock_core.platformio_cache_dir = str(platformio_cache_dir)
# Verify all exist before
assert pioenvs_dir.exists()