Compare commits

..

79 Commits

Author SHA1 Message Date
J. Nick Koston
935b4c3788 fix 2026-01-20 12:51:57 -10:00
J. Nick Koston
cfa9c3ed49 Update esphome/components/api/api_frame_helper_noise.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-20 12:46:17 -10:00
J. Nick Koston
d8a38815fd missed one 2026-01-20 12:43:38 -10:00
J. Nick Koston
bc776ffd59 we have one 2026-01-20 12:41:10 -10:00
J. Nick Koston
fd0ea32100 [api] Use stack buffers for noise handshake messages 2026-01-20 12:38:58 -10:00
Jonathan Swoboda
6edecd3d45 Merge branch 'beta' into dev 2026-01-20 17:01:47 -05:00
Jonathan Swoboda
055c00f1ac Merge pull request #13396 from esphome/bump-2026.1.0b4
2026.1.0b4
2026-01-20 17:01:36 -05:00
Jonathan Swoboda
7dc40881e2 Bump version to 2026.1.0b4 2026-01-20 15:55:03 -05:00
J. Nick Koston
b04373687e [wifi_info] Fix missing state when both IP+DNS or SSID+BSSID configure (#13385) 2026-01-20 15:55:03 -05:00
Jonathan Swoboda
b89c127f62 [x9c] Fix potentiometer unable to decrement (#13382)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 15:55:03 -05:00
Jonathan Swoboda
47dc5d0a1f [core] Fix state leakage and module caching when processing multiple configurations (#13368)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 15:55:03 -05:00
J. Nick Koston
21886dd3ac [api] Fix truncation of Home Assistant attributes longer than 255 characters (#13348) 2026-01-20 15:55:03 -05:00
J. Nick Koston
85a5a26519 [network] Fix IPAddress::str_to() to lowercase IPv6 hex digits (#13325) 2026-01-20 15:55:03 -05:00
Clyde Stubbs
79ccacd6d6 [helpers] Allow reading capacity of FixedVector (#13391) 2026-01-20 09:24:42 -10:00
J. Nick Koston
e2319ba651 [wifi_info] Fix missing state when both IP+DNS or SSID+BSSID configure (#13385) 2026-01-20 07:55:59 -10:00
Jonathan Swoboda
ed4ebffa74 [x9c] Fix potentiometer unable to decrement (#13382)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 22:57:54 -05:00
J. Nick Koston
c213de4861 [mapping] Use stack buffers for numeric key error logging (#13299) 2026-01-19 17:42:08 -10:00
J. Nick Koston
6cf320fd60 [mqtt] Eliminate per-entity loop overhead and heap churn (#13356) 2026-01-19 17:41:55 -10:00
J. Nick Koston
aeea340bc6 [cs5460a] Remove unnecessary empty loop override (#13357) 2026-01-19 17:41:03 -10:00
J. Nick Koston
d0e50ed030 [lock] Extract set_state_ helper to reduce code duplication (#13359) 2026-01-19 17:40:51 -10:00
J. Nick Koston
280d460025 [statsd] Use direct appends and stack buffer instead of str_sprintf (#13223)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-19 17:40:20 -10:00
J. Nick Koston
ea70faf642 [debug] Use shared buf_append_printf helper from core (#13260) 2026-01-19 17:38:56 -10:00
J. Nick Koston
5d7b38b261 [ezo_pmp] Replace sprintf with bounds-checked snprintf (#13304) 2026-01-19 17:38:22 -10:00
J. Nick Koston
e88093ca60 [am43][lightwaverf][rf_bridge][spi_led_strip] Replace sprintf with safe alternatives (#13302) 2026-01-19 17:38:08 -10:00
J. Nick Koston
b48d4ab785 [mqtt] Reduce heap allocations in publish path (#13372) 2026-01-19 17:37:54 -10:00
J. Nick Koston
8ade9dfc10 [shtcx] Use LogString for type to_string to save RAM on ESP8266 (#13370) 2026-01-19 17:37:33 -10:00
J. Nick Koston
4e0e7796de [mqtt] Remove unnecessary defer in ESP8266 on_message callback (#13373) 2026-01-19 17:37:19 -10:00
J. Nick Koston
62b6c9bf7c [esp32_ble] Deprecate ESPBTUUID::to_string() in favor of heap-free to_str() (#13376) 2026-01-19 17:37:03 -10:00
J. Nick Koston
b5fe271d6b [sprinkler] Disable loops when idle to reduce CPU overhead (#13381) 2026-01-19 17:36:47 -10:00
J. Nick Koston
5d787e2512 [sprinkler] Eliminate std::string heap allocations (#13379) 2026-01-19 17:35:58 -10:00
J. Nick Koston
8998ef0bc3 [network] Deprecate IPAddress::str() in favor of heap-free str_to() (#13378) 2026-01-19 17:35:32 -10:00
J. Nick Koston
8ec31dd769 [voice_assistant] Deprecate Timer::to_string() in favor of heap-free to_str() (#13377) 2026-01-19 17:35:19 -10:00
J. Nick Koston
0193464f92 [dsmr] Avoid std::string allocation for decryption key (#13375) 2026-01-19 17:34:49 -10:00
Jonathan Swoboda
1996bc425f [core] Fix state leakage and module caching when processing multiple configurations (#13368)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 14:46:24 -05:00
Clyde Stubbs
a0d3d54d69 [mipi_spi] Add variants of ESP32-2432S028 displays (#13340) 2026-01-20 05:13:36 +11:00
J. Nick Koston
ee264d0fd4 [anova] Replace sprintf with bounds-checked alternatives (#13303) 2026-01-18 23:57:42 -10:00
J. Nick Koston
892e9b006f [api] Use MAX_STATE_LEN constant for Home Assistant state buffer (#13278) 2026-01-18 23:57:27 -10:00
J. Nick Koston
f8bd4ef57d [template][event] Use StringRef for set_action and on_event triggers (#13328) 2026-01-18 22:22:57 -10:00
J. Nick Koston
bfcc0e26a3 [dfrobot_sen0395][pipsolar][sim800l][wl_134] Replace sprintf with snprintf/buf_append_printf (#13301)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-18 22:22:44 -10:00
J. Nick Koston
86a1b4cf69 [select][fan] Use StringRef for on_value/on_preset_set triggers to avoid heap allocation (#13324) 2026-01-18 19:51:11 -10:00
J. Nick Koston
d8a28f6fba [scheduler] Replace resize() with erase() to save ~ 436 bytes flash (#13214) 2026-01-18 18:54:30 -10:00
J. Nick Koston
e80a940222 [gdk101] Use stack buffer to eliminate heap allocation for firmware version (#13224) 2026-01-18 18:52:49 -10:00
J. Nick Koston
e99dbe05f7 [toshiba] Replace to_string with stack buffer in debug logging (#13296) 2026-01-18 18:52:34 -10:00
J. Nick Koston
f453a8d9a1 [dfrobot_sen0395] Reduce heap allocations in command building (#13219) 2026-01-18 18:44:56 -10:00
J. Nick Koston
126190d26a [ezo] Replace str_sprintf with stack-based formatting (#13218)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-18 18:44:41 -10:00
J. Nick Koston
e40201a98d [cse7766] Use stack buffer for verbose debug logging (#13217) 2026-01-18 18:44:27 -10:00
J. Nick Koston
8142f5db44 [zephyr] Avoid heap allocation in preferences key formatting (#13215) 2026-01-18 18:43:50 -10:00
J. Nick Koston
98ccab87a7 [tormatic] Use stack buffers instead of str_sprintf in debug methods (#13225) 2026-01-18 18:43:36 -10:00
J. Nick Koston
b9e72a8774 [daikin_arc] Fix undefined behavior in sprintf calls (#13279)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-18 18:43:19 -10:00
J. Nick Koston
d9fc625c6a [web_server] Simplify datetime formatting with buf_append_printf (#13281) 2026-01-18 18:43:05 -10:00
J. Nick Koston
dfbf79d6d6 [homeassistant] Use buf_append_printf for ESP8266 flash optimization (#13284) 2026-01-18 18:42:19 -10:00
J. Nick Koston
ea0fac96cb [core][mqtt] Add str_sanitize_to(), soft-deprecate str_sanitize() (#13233) 2026-01-18 18:42:04 -10:00
J. Nick Koston
3182222d60 [esp32_hosted] Use stack buffer instead of str_sprintf for version string (#13226) 2026-01-18 18:41:47 -10:00
J. Nick Koston
d8849b16f2 [gpio] Use buf_append_printf in dump_summary for ESP8266 flash optimization (#13283) 2026-01-18 18:41:34 -10:00
J. Nick Koston
635983f163 [uptime] Use buf_append_printf for ESP8266 flash optimization (#13282) 2026-01-18 18:41:19 -10:00
J. Nick Koston
6cbe672004 [tuya] Use buf_append_printf for ESP8266 flash optimization (#13287) 2026-01-18 18:41:07 -10:00
J. Nick Koston
226867b05c [esp8266] Use direct SDK calls instead of Arduino ESP class wrappers (#13353) 2026-01-18 18:40:53 -10:00
J. Nick Koston
67871a1683 [ccs811] Use buf_append_printf for buffer safety and ESP8266 flash optimization (#13300) 2026-01-18 18:40:14 -10:00
J. Nick Koston
f60c03e350 [syslog] Use buf_append_printf for ESP8266 flash optimization (#13286) 2026-01-18 18:39:53 -10:00
J. Nick Koston
eb66429144 [sml] Use stack buffers instead of str_sprintf (#13222)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-18 18:39:23 -10:00
J. Nick Koston
0f3bac5dd6 [nextion] Replace to_string with stack buffer and fix unsafe sprintf (#13295) 2026-01-18 18:37:29 -10:00
J. Nick Koston
5b92d0b89e [wiegand] Replace heap-allocating to_string with stack buffers (#13294) 2026-01-18 18:37:14 -10:00
J. Nick Koston
052b05df56 [tuya] Replace unsafe sprintf with snprintf in light color formatting (#13292) 2026-01-18 18:37:02 -10:00
J. Nick Koston
7b0db659d1 [rc522_spi] Replace unsafe sprintf with buf_append_printf (#13291) 2026-01-18 18:36:46 -10:00
J. Nick Koston
2f7270cf8f [uart] Replace unsafe sprintf with buf_append_printf in debugger (#13288) 2026-01-18 18:36:32 -10:00
J. Nick Koston
b44727aee6 [socket] Eliminate heap allocations in set_sockaddr() (#13228) 2026-01-18 18:29:31 -10:00
J. Nick Koston
1a55254258 [status] Convert to PollingComponent to reduce CPU usage (#13342) 2026-01-18 18:28:24 -10:00
J. Nick Koston
baf2b0e3c9 [api] Fix truncation of Home Assistant attributes longer than 255 characters (#13348) 2026-01-18 18:23:11 -10:00
J. Nick Koston
680e92a226 [core] Add str_endswith_ignore_case to avoid heap allocation in audio file type detection (#13313) 2026-01-18 08:36:56 -10:00
J. Nick Koston
db0b32bfc9 [network] Fix IPAddress::str_to() to lowercase IPv6 hex digits (#13325) 2026-01-17 18:06:54 -10:00
J. Nick Koston
21794e28e5 [modbus_controller] Use stack buffers instead of heap-allocating string helpers (#13221)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-01-17 17:26:51 -10:00
J. Nick Koston
728236270c [weikai] Replace bitset to_string with format_bin_to (#13297) 2026-01-17 15:53:01 -10:00
J. Nick Koston
01cdc4ed58 [core] Add fnv1_hash_extend() string overloads, use in atm90e32 (#13326) 2026-01-17 15:52:19 -10:00
J. Nick Koston
d6a0c8ffbb [template] Store alarm control panel codes in flash instead of heap (#13329) 2026-01-17 15:52:06 -10:00
J. Nick Koston
4cc0f874f7 [wireguard] Store configuration strings in flash instead of heap (#13331) 2026-01-17 15:51:26 -10:00
J. Nick Koston
ed58b9372f [template] Store text initial_value in flash and avoid heap allocation in setup (#13332) 2026-01-17 15:51:12 -10:00
J. Nick Koston
ee2a81923b [sun] Store text sensor format string in flash (#13335) 2026-01-17 15:51:01 -10:00
J. Nick Koston
0a1e7ee50b [pipsolar] Store command strings in flash (#13336) 2026-01-17 15:50:42 -10:00
J. Nick Koston
4d4283bcfa [udp] Store addresses in flash instead of heap (#13330) 2026-01-17 15:50:23 -10:00
107 changed files with 702 additions and 1138 deletions

View File

@@ -1,5 +1,6 @@
# PYTHON_ARGCOMPLETE_OK
import argparse
from collections.abc import Callable
from datetime import datetime
import functools
import getpass
@@ -800,13 +801,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
@@ -943,11 +937,21 @@ def command_dashboard(args: ArgsProtocol) -> int | None:
return dashboard.start_dashboard(args)
def command_update_all(args: ArgsProtocol) -> int | None:
def run_multiple_configs(
files: list, command_builder: Callable[[str], list[str]]
) -> int:
"""Run a command for each configuration file in a subprocess.
Args:
files: List of configuration files to process.
command_builder: Callable that takes a file path and returns a command list.
Returns:
Number of failed files.
"""
import click
success = {}
files = list_yaml_files(args.configuration)
twidth = 60
def print_bar(middle_text):
@@ -957,17 +961,19 @@ def command_update_all(args: ArgsProtocol) -> int | None:
safe_print(f"{half_line}{middle_text}{half_line}")
for f in files:
safe_print(f"Updating {color(AnsiFore.CYAN, str(f))}")
f_path = Path(f) if not isinstance(f, Path) else f
if any(f_path.name == x for x in SECRETS_FILES):
_LOGGER.warning("Skipping secrets file %s", f_path)
continue
safe_print(f"Processing {color(AnsiFore.CYAN, str(f))}")
safe_print("-" * twidth)
safe_print()
if CORE.dashboard:
rc = run_external_process(
"esphome", "--dashboard", "run", f, "--no-logs", "--device", "OTA"
)
else:
rc = run_external_process(
"esphome", "run", f, "--no-logs", "--device", "OTA"
)
cmd = command_builder(f)
rc = run_external_process(*cmd)
if rc == 0:
print_bar(f"[{color(AnsiFore.BOLD_GREEN, 'SUCCESS')}] {str(f)}")
success[f] = True
@@ -982,6 +988,8 @@ def command_update_all(args: ArgsProtocol) -> int | None:
print_bar(f"[{color(AnsiFore.BOLD_WHITE, 'SUMMARY')}]")
failed = 0
for f in files:
if f not in success:
continue # Skipped file
if success[f]:
safe_print(f" - {str(f)}: {color(AnsiFore.GREEN, 'SUCCESS')}")
else:
@@ -990,6 +998,17 @@ def command_update_all(args: ArgsProtocol) -> int | None:
return failed
def command_update_all(args: ArgsProtocol) -> int | None:
files = list_yaml_files(args.configuration)
def build_command(f):
if CORE.dashboard:
return ["esphome", "--dashboard", "run", f, "--no-logs", "--device", "OTA"]
return ["esphome", "run", f, "--no-logs", "--device", "OTA"]
return run_multiple_configs(files, build_command)
def command_idedata(args: ArgsProtocol, config: ConfigType) -> int:
import json
@@ -1291,17 +1310,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",
@@ -1551,38 +1559,48 @@ def run_esphome(argv):
_LOGGER.info("ESPHome %s", const.__version__)
for conf_path in args.configuration:
conf_path = Path(conf_path)
if any(conf_path.name == x for x in SECRETS_FILES):
_LOGGER.warning("Skipping secrets file %s", conf_path)
continue
# Multiple configurations: use subprocesses to avoid state leakage
# between compilations (e.g., LVGL touchscreen state in module globals)
if len(args.configuration) > 1:
# Build command by reusing argv, replacing all configs with single file
# argv[0] is the program path, skip it since we prefix with "esphome"
def build_command(f):
return (
["esphome"]
+ [arg for arg in argv[1:] if arg not in args.configuration]
+ [str(f)]
)
CORE.config_path = conf_path
CORE.dashboard = args.dashboard
return run_multiple_configs(args.configuration, build_command)
# For logs command, skip updating external components
skip_external = args.command == "logs"
config = read_config(
dict(args.substitution) if args.substitution else {},
skip_external_update=skip_external,
)
if config is None:
return 2
CORE.config = config
# Single configuration
conf_path = Path(args.configuration[0])
if any(conf_path.name == x for x in SECRETS_FILES):
_LOGGER.warning("Skipping secrets file %s", conf_path)
return 0
if args.command not in POST_CONFIG_ACTIONS:
safe_print(f"Unknown command {args.command}")
CORE.config_path = conf_path
CORE.dashboard = args.dashboard
try:
rc = POST_CONFIG_ACTIONS[args.command](args, config)
except EsphomeError as e:
_LOGGER.error(e, exc_info=args.verbose)
return 1
if rc != 0:
return rc
# For logs command, skip updating external components
skip_external = args.command == "logs"
config = read_config(
dict(args.substitution) if args.substitution else {},
skip_external_update=skip_external,
)
if config is None:
return 2
CORE.config = config
CORE.reset()
return 0
if args.command not in POST_CONFIG_ACTIONS:
safe_print(f"Unknown command {args.command}")
return 1
try:
return POST_CONFIG_ACTIONS[args.command](args, config)
except EsphomeError as e:
_LOGGER.error(e, exc_info=args.verbose)
return 1
def main():

View File

@@ -4,7 +4,6 @@ from __future__ import annotations
from collections import defaultdict
from collections.abc import Callable
import json
import sys
from typing import TYPE_CHECKING
@@ -439,28 +438,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

@@ -7,12 +7,6 @@ namespace am43 {
const uint8_t START_PACKET[5] = {0x00, 0xff, 0x00, 0x00, 0x9a};
std::string pkt_to_hex(const uint8_t *data, uint16_t len) {
char buf[64]; // format_hex_size(31) = 63, fits 31 bytes of hex data
format_hex_to(buf, sizeof(buf), data, len);
return buf;
}
Am43Packet *Am43Encoder::get_battery_level_request() {
uint8_t data = 0x1;
return this->encode_(0xA2, &data, 1);
@@ -70,7 +64,9 @@ Am43Packet *Am43Encoder::encode_(uint8_t command, uint8_t *data, uint8_t length)
memcpy(&this->packet_.data[7], data, length);
this->packet_.length = length + 7;
this->checksum_();
ESP_LOGV("am43", "ENC(%d): 0x%s", packet_.length, pkt_to_hex(packet_.data, packet_.length).c_str());
char hex_buf[format_hex_size(sizeof(this->packet_.data))];
ESP_LOGV("am43", "ENC(%d): 0x%s", this->packet_.length,
format_hex_to(hex_buf, this->packet_.data, this->packet_.length));
return &this->packet_;
}
@@ -85,7 +81,8 @@ void Am43Decoder::decode(const uint8_t *data, uint16_t length) {
this->has_set_state_response_ = false;
this->has_position_ = false;
this->has_pin_response_ = false;
ESP_LOGV("am43", "DEC(%d): 0x%s", length, pkt_to_hex(data, length).c_str());
char hex_buf[format_hex_size(24)]; // Max expected packet size
ESP_LOGV("am43", "DEC(%d): 0x%s", length, format_hex_to(hex_buf, data, length));
if (length < 2 || data[0] != 0x9a)
return;

View File

@@ -18,31 +18,31 @@ AnovaPacket *AnovaCodec::clean_packet_() {
AnovaPacket *AnovaCodec::get_read_device_status_request() {
this->current_query_ = READ_DEVICE_STATUS;
strncpy((char *) this->packet_.data, CMD_READ_DEVICE_STATUS, sizeof(this->packet_.data));
snprintf((char *) this->packet_.data, sizeof(this->packet_.data), "%s", CMD_READ_DEVICE_STATUS);
return this->clean_packet_();
}
AnovaPacket *AnovaCodec::get_read_target_temp_request() {
this->current_query_ = READ_TARGET_TEMPERATURE;
strncpy((char *) this->packet_.data, CMD_READ_TARGET_TEMP, sizeof(this->packet_.data));
snprintf((char *) this->packet_.data, sizeof(this->packet_.data), "%s", CMD_READ_TARGET_TEMP);
return this->clean_packet_();
}
AnovaPacket *AnovaCodec::get_read_current_temp_request() {
this->current_query_ = READ_CURRENT_TEMPERATURE;
strncpy((char *) this->packet_.data, CMD_READ_CURRENT_TEMP, sizeof(this->packet_.data));
snprintf((char *) this->packet_.data, sizeof(this->packet_.data), "%s", CMD_READ_CURRENT_TEMP);
return this->clean_packet_();
}
AnovaPacket *AnovaCodec::get_read_unit_request() {
this->current_query_ = READ_UNIT;
strncpy((char *) this->packet_.data, CMD_READ_UNIT, sizeof(this->packet_.data));
snprintf((char *) this->packet_.data, sizeof(this->packet_.data), "%s", CMD_READ_UNIT);
return this->clean_packet_();
}
AnovaPacket *AnovaCodec::get_read_data_request() {
this->current_query_ = READ_DATA;
strncpy((char *) this->packet_.data, CMD_READ_DATA, sizeof(this->packet_.data));
snprintf((char *) this->packet_.data, sizeof(this->packet_.data), "%s", CMD_READ_DATA);
return this->clean_packet_();
}
@@ -62,13 +62,13 @@ AnovaPacket *AnovaCodec::get_set_unit_request(char unit) {
AnovaPacket *AnovaCodec::get_start_request() {
this->current_query_ = START;
strncpy((char *) this->packet_.data, CMD_START, sizeof(this->packet_.data));
snprintf((char *) this->packet_.data, sizeof(this->packet_.data), "%s", CMD_START);
return this->clean_packet_();
}
AnovaPacket *AnovaCodec::get_stop_request() {
this->current_query_ = STOP;
strncpy((char *) this->packet_.data, CMD_STOP, sizeof(this->packet_.data));
snprintf((char *) this->packet_.data, sizeof(this->packet_.data), "%s", CMD_STOP);
return this->clean_packet_();
}

View File

@@ -1712,17 +1712,16 @@ void APIConnection::on_home_assistant_state_response(const HomeAssistantStateRes
}
// Create null-terminated state for callback (parse_number needs null-termination)
// HA state max length is 255, so 256 byte buffer covers all cases
char state_buf[256];
size_t copy_len = msg.state.size();
if (copy_len >= sizeof(state_buf)) {
copy_len = sizeof(state_buf) - 1; // Truncate to leave space for null terminator
// HA state max length is 255 characters, but attributes can be much longer
// Use stack buffer for common case (states), heap fallback for large attributes
size_t state_len = msg.state.size();
SmallBufferWithHeapFallback<MAX_STATE_LEN + 1> state_buf_alloc(state_len + 1);
char *state_buf = reinterpret_cast<char *>(state_buf_alloc.get());
if (state_len > 0) {
memcpy(state_buf, msg.state.c_str(), state_len);
}
if (copy_len > 0) {
memcpy(state_buf, msg.state.c_str(), copy_len);
}
state_buf[copy_len] = '\0';
it.callback(StringRef(state_buf, copy_len));
state_buf[state_len] = '\0';
it.callback(StringRef(state_buf, state_len));
}
}
#endif

View File

@@ -3,6 +3,7 @@
#ifdef USE_API_NOISE
#include "api_connection.h" // For ClientInfo struct
#include "esphome/core/application.h"
#include "esphome/core/entity_base.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -256,28 +257,30 @@ APIError APINoiseFrameHelper::state_action_() {
}
if (state_ == State::SERVER_HELLO) {
// send server hello
constexpr size_t mac_len = 13; // 12 hex chars + null terminator
const std::string &name = App.get_name();
char mac[mac_len];
char mac[MAC_ADDRESS_BUFFER_SIZE];
get_mac_address_into_buffer(mac);
// Calculate positions and sizes
size_t name_len = name.size() + 1; // including null terminator
size_t name_offset = 1;
size_t mac_offset = name_offset + name_len;
size_t total_size = 1 + name_len + mac_len;
size_t total_size = 1 + name_len + MAC_ADDRESS_BUFFER_SIZE;
auto msg = std::make_unique<uint8_t[]>(total_size);
// 1 (proto) + name (max ESPHOME_DEVICE_NAME_MAX_LEN) + 1 (name null)
// + mac (MAC_ADDRESS_BUFFER_SIZE - 1) + 1 (mac null)
constexpr size_t max_msg_size = 1 + ESPHOME_DEVICE_NAME_MAX_LEN + 1 + MAC_ADDRESS_BUFFER_SIZE;
uint8_t msg[max_msg_size];
// chosen proto
msg[0] = 0x01;
// node name, terminated by null byte
std::memcpy(msg.get() + name_offset, name.c_str(), name_len);
std::memcpy(msg + name_offset, name.c_str(), name_len);
// node mac, terminated by null byte
std::memcpy(msg.get() + mac_offset, mac, mac_len);
std::memcpy(msg + mac_offset, mac, MAC_ADDRESS_BUFFER_SIZE);
aerr = write_frame_(msg.get(), total_size);
aerr = write_frame_(msg, total_size);
if (aerr != APIError::OK)
return aerr;
@@ -353,35 +356,32 @@ APIError APINoiseFrameHelper::state_action_() {
return APIError::OK;
}
void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reason) {
// Max reject message: "Bad handshake packet len" (24) + 1 (failure byte) = 25 bytes
uint8_t data[32];
data[0] = 0x01; // failure
#ifdef USE_STORE_LOG_STR_IN_FLASH
// On ESP8266 with flash strings, we need to use PROGMEM-aware functions
size_t reason_len = strlen_P(reinterpret_cast<PGM_P>(reason));
size_t data_size = reason_len + 1;
auto data = std::make_unique<uint8_t[]>(data_size);
data[0] = 0x01; // failure
// Copy error message from PROGMEM
if (reason_len > 0) {
memcpy_P(data.get() + 1, reinterpret_cast<PGM_P>(reason), reason_len);
memcpy_P(data + 1, reinterpret_cast<PGM_P>(reason), reason_len);
}
#else
// Normal memory access
const char *reason_str = LOG_STR_ARG(reason);
size_t reason_len = strlen(reason_str);
size_t data_size = reason_len + 1;
auto data = std::make_unique<uint8_t[]>(data_size);
data[0] = 0x01; // failure
// Copy error message in bulk
if (reason_len > 0) {
std::memcpy(data.get() + 1, reason_str, reason_len);
// NOLINTNEXTLINE(bugprone-not-null-terminated-result) - binary protocol, not a C string
std::memcpy(data + 1, reason_str, reason_len);
}
#endif
size_t data_size = reason_len + 1;
// temporarily remove failed state
auto orig_state = state_;
state_ = State::EXPLICIT_REJECT;
write_frame_(data.get(), data_size);
write_frame_(data, data_size);
state_ = orig_state;
}
APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {

View File

@@ -3,7 +3,6 @@
#include "bedjet_hub.h"
#include "bedjet_child.h"
#include "bedjet_const.h"
#include "esphome/components/esp32_ble/ble_uuid.h"
#include "esphome/core/application.h"
#include <cinttypes>

View File

@@ -76,7 +76,6 @@ class CS5460AComponent : public Component,
void restart() { restart_(); }
void setup() override;
void loop() override {}
void dump_config() override;
protected:

View File

@@ -63,13 +63,11 @@ def validate_auto_clear(value):
return cv.boolean(value)
def basic_display_schema(default_update_interval: str = "1s") -> cv.Schema:
"""Create a basic display schema with configurable default update interval."""
return cv.Schema(
{
cv.Exclusive(CONF_LAMBDA, CONF_LAMBDA): cv.lambda_,
}
).extend(cv.polling_component_schema(default_update_interval))
BASIC_DISPLAY_SCHEMA = cv.Schema(
{
cv.Exclusive(CONF_LAMBDA, CONF_LAMBDA): cv.lambda_,
}
).extend(cv.polling_component_schema("1s"))
def _validate_test_card(config):
@@ -83,41 +81,34 @@ def _validate_test_card(config):
return config
def full_display_schema(default_update_interval: str = "1s") -> cv.Schema:
"""Create a full display schema with configurable default update interval."""
schema = basic_display_schema(default_update_interval).extend(
{
cv.Optional(CONF_ROTATION): validate_rotation,
cv.Exclusive(CONF_PAGES, CONF_LAMBDA): cv.All(
cv.ensure_list(
{
cv.GenerateID(): cv.declare_id(DisplayPage),
cv.Required(CONF_LAMBDA): cv.lambda_,
}
),
cv.Length(min=1),
),
cv.Optional(CONF_ON_PAGE_CHANGE): automation.validate_automation(
FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend(
{
cv.Optional(CONF_ROTATION): validate_rotation,
cv.Exclusive(CONF_PAGES, CONF_LAMBDA): cv.All(
cv.ensure_list(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
DisplayOnPageChangeTrigger
),
cv.Optional(CONF_FROM): cv.use_id(DisplayPage),
cv.Optional(CONF_TO): cv.use_id(DisplayPage),
cv.GenerateID(): cv.declare_id(DisplayPage),
cv.Required(CONF_LAMBDA): cv.lambda_,
}
),
cv.Optional(
CONF_AUTO_CLEAR_ENABLED, default=CONF_UNSPECIFIED
): validate_auto_clear,
cv.Optional(CONF_SHOW_TEST_CARD): cv.boolean,
}
)
schema.add_extra(_validate_test_card)
return schema
BASIC_DISPLAY_SCHEMA = basic_display_schema("1s")
FULL_DISPLAY_SCHEMA = full_display_schema("1s")
cv.Length(min=1),
),
cv.Optional(CONF_ON_PAGE_CHANGE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
DisplayOnPageChangeTrigger
),
cv.Optional(CONF_FROM): cv.use_id(DisplayPage),
cv.Optional(CONF_TO): cv.use_id(DisplayPage),
}
),
cv.Optional(
CONF_AUTO_CLEAR_ENABLED, default=CONF_UNSPECIFIED
): validate_auto_clear,
cv.Optional(CONF_SHOW_TEST_CARD): cv.boolean,
}
)
FULL_DISPLAY_SCHEMA.add_extra(_validate_test_card)
async def setup_display_core_(var, config):

View File

@@ -25,29 +25,13 @@ dsmr_ns = cg.esphome_ns.namespace("esphome::dsmr")
Dsmr = dsmr_ns.class_("Dsmr", cg.Component, uart.UARTDevice)
def _validate_key(value):
value = cv.string_strict(value)
parts = [value[i : i + 2] for i in range(0, len(value), 2)]
if len(parts) != 16:
raise cv.Invalid("Decryption key must consist of 16 hexadecimal numbers")
parts_int = []
if any(len(part) != 2 for part in parts):
raise cv.Invalid("Decryption key must be format XX")
for part in parts:
try:
parts_int.append(int(part, 16))
except ValueError:
# pylint: disable=raise-missing-from
raise cv.Invalid("Decryption key must be hex values from 00 to FF")
return "".join(f"{part:02X}" for part in parts_int)
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(Dsmr),
cv.Optional(CONF_DECRYPTION_KEY): _validate_key,
cv.Optional(CONF_DECRYPTION_KEY): lambda value: cv.bind_key(
value, name="Decryption key"
),
cv.Optional(CONF_CRC_CHECK, default=True): cv.boolean,
cv.Optional(CONF_GAS_MBUS_ID, default=1): cv.int_,
cv.Optional(CONF_WATER_MBUS_ID, default=2): cv.int_,

View File

@@ -1,4 +1,5 @@
#include "dsmr.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <AES.h>
@@ -294,8 +295,8 @@ void Dsmr::dump_config() {
DSMR_TEXT_SENSOR_LIST(DSMR_LOG_TEXT_SENSOR, )
}
void Dsmr::set_decryption_key(const std::string &decryption_key) {
if (decryption_key.empty()) {
void Dsmr::set_decryption_key(const char *decryption_key) {
if (decryption_key == nullptr || decryption_key[0] == '\0') {
ESP_LOGI(TAG, "Disabling decryption");
this->decryption_key_.clear();
if (this->crypt_telegram_ != nullptr) {
@@ -305,21 +306,15 @@ void Dsmr::set_decryption_key(const std::string &decryption_key) {
return;
}
if (decryption_key.length() != 32) {
ESP_LOGE(TAG, "Error, decryption key must be 32 character long");
if (!parse_hex(decryption_key, this->decryption_key_, 16)) {
ESP_LOGE(TAG, "Error, decryption key must be 32 hex characters");
this->decryption_key_.clear();
return;
}
this->decryption_key_.clear();
ESP_LOGI(TAG, "Decryption key is set");
// Verbose level prints decryption key
ESP_LOGV(TAG, "Using decryption key: %s", decryption_key.c_str());
char temp[3] = {0};
for (int i = 0; i < 16; i++) {
strncpy(temp, &(decryption_key.c_str()[i * 2]), 2);
this->decryption_key_.push_back(std::strtoul(temp, nullptr, 16));
}
ESP_LOGV(TAG, "Using decryption key: %s", decryption_key);
if (this->crypt_telegram_ == nullptr) {
this->crypt_telegram_ = new uint8_t[this->max_telegram_len_]; // NOLINT

View File

@@ -63,7 +63,7 @@ class Dsmr : public Component, public uart::UARTDevice {
void dump_config() override;
void set_decryption_key(const std::string &decryption_key);
void set_decryption_key(const char *decryption_key);
void set_max_telegram_length(size_t length) { this->max_telegram_len_ = length; }
void set_request_pin(GPIOPin *request_pin) { this->request_pin_ = request_pin; }
void set_request_interval(uint32_t interval) { this->request_interval_ = interval; }

View File

@@ -31,7 +31,6 @@ from esphome.const import (
CONF_TRANSFORM,
CONF_UPDATE_INTERVAL,
CONF_WIDTH,
SCHEDULER_DONT_RUN,
)
from esphome.cpp_generator import RawExpression
from esphome.final_validate import full_config
@@ -73,10 +72,12 @@ TRANSFORM_OPTIONS = {CONF_MIRROR_X, CONF_MIRROR_Y, CONF_SWAP_XY}
def model_schema(config):
model = MODELS[config[CONF_MODEL]]
class_name = epaper_spi_ns.class_(model.class_name, EPaperBase)
minimum_update_interval = update_interval(
model.get_default(CONF_MINIMUM_UPDATE_INTERVAL, "1s")
)
cv_dimensions = cv.Optional if model.get_default(CONF_WIDTH) else cv.Required
return (
display.full_display_schema("60s")
.extend(
display.FULL_DISPLAY_SCHEMA.extend(
spi.spi_device_schema(
cs_pin_required=False,
default_mode="MODE0",
@@ -93,6 +94,9 @@ def model_schema(config):
{
cv.Optional(CONF_ROTATION, default=0): validate_rotation,
cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True),
cv.Optional(CONF_UPDATE_INTERVAL, default=cv.UNDEFINED): cv.All(
update_interval, cv.Range(min=minimum_update_interval)
),
cv.Optional(CONF_TRANSFORM): cv.Schema(
{
cv.Required(CONF_MIRROR_X): cv.boolean,
@@ -146,22 +150,15 @@ def _final_validate(config):
global_config = full_config.get()
from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN
# If no drawing methods are configured, and LVGL is not enabled, show a test card
if (
CONF_LAMBDA not in config
and CONF_PAGES not in config
and LVGL_DOMAIN not in global_config
):
config[CONF_SHOW_TEST_CARD] = True
interval = config[CONF_UPDATE_INTERVAL]
if interval != SCHEDULER_DONT_RUN:
model = MODELS[config[CONF_MODEL]]
minimum = update_interval(model.get_default(CONF_MINIMUM_UPDATE_INTERVAL, "1s"))
if interval < minimum:
raise cv.Invalid(
f"update_interval must be at least {minimum} for {model.name}, got {interval}"
)
if CONF_LAMBDA not in config and CONF_PAGES not in config:
if LVGL_DOMAIN in global_config:
if CONF_UPDATE_INTERVAL not in config:
config[CONF_UPDATE_INTERVAL] = update_interval("never")
else:
# If no drawing methods are configured, and LVGL is not enabled, show a test card
config[CONF_SHOW_TEST_CARD] = True
elif CONF_UPDATE_INTERVAL not in config:
config[CONF_UPDATE_INTERVAL] = update_interval("1min")
return config

View File

@@ -660,9 +660,6 @@ CONF_LOOP_TASK_STACK_SIZE = "loop_task_stack_size"
KEY_VFS_SELECT_REQUIRED = "vfs_select_required"
KEY_VFS_DIR_REQUIRED = "vfs_dir_required"
# Ring buffer IRAM requirement tracking
KEY_RINGBUF_IN_IRAM = "ringbuf_in_iram"
def require_vfs_select() -> None:
"""Mark that VFS select support is required by a component.
@@ -682,17 +679,6 @@ def require_vfs_dir() -> None:
CORE.data[KEY_VFS_DIR_REQUIRED] = True
def enable_ringbuf_in_iram() -> None:
"""Keep ring buffer functions in IRAM instead of moving them to flash.
Call this from components that use esphome/core/ring_buffer.cpp and need
the ring buffer functions to remain in IRAM for performance reasons
(e.g., voice assistants, audio components).
This prevents CONFIG_RINGBUF_PLACE_FUNCTIONS_INTO_FLASH from being enabled.
"""
CORE.data[KEY_RINGBUF_IN_IRAM] = True
def _parse_idf_component(value: str) -> ConfigType:
"""Parse IDF component shorthand syntax like 'owner/component^version'"""
# Match operator followed by version-like string (digit or *)
@@ -1104,18 +1090,14 @@ async def to_code(config):
add_idf_sdkconfig_option("CONFIG_FREERTOS_PLACE_FUNCTIONS_INTO_FLASH", True)
# Place ring buffer functions into flash instead of IRAM by default
# This saves IRAM but may impact performance for audio/voice components.
# Components that need ring buffer in IRAM call enable_ringbuf_in_iram().
# Users can also set ringbuf_in_iram: true to force IRAM placement.
# In ESP-IDF 6.0 flash placement becomes the default.
if conf[CONF_ADVANCED][CONF_RINGBUF_IN_IRAM] or CORE.data.get(
KEY_RINGBUF_IN_IRAM, False
):
# User config or component requires ring buffer in IRAM for performance
# This saves IRAM. In ESP-IDF 6.0 flash placement becomes the default.
# Users can set ringbuf_in_iram: true as an escape hatch if they encounter issues.
if conf[CONF_ADVANCED][CONF_RINGBUF_IN_IRAM]:
# User requests ring buffer in IRAM
# IDF 6.0+: will need CONFIG_RINGBUF_PLACE_ISR_FUNCTIONS_INTO_FLASH=n
add_idf_sdkconfig_option("CONFIG_RINGBUF_PLACE_ISR_FUNCTIONS_INTO_FLASH", False)
else:
# No component needs it - place in flash to save IRAM
# Place in flash to save IRAM (default)
add_idf_sdkconfig_option("CONFIG_RINGBUF_PLACE_FUNCTIONS_INTO_FLASH", True)
# Place heap functions into flash to save IRAM (~4-6KB savings)

View File

@@ -85,6 +85,7 @@ void ESP32InternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpi
break;
}
gpio_set_intr_type(this->get_pin_num(), idf_type);
gpio_intr_enable(this->get_pin_num());
if (!isr_service_installed) {
auto res = gpio_install_isr_service(ESP_INTR_FLAG_LEVEL3);
if (res != ESP_OK) {
@@ -94,7 +95,6 @@ void ESP32InternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpi
isr_service_installed = true;
}
gpio_isr_handler_add(this->get_pin_num(), func, arg);
gpio_intr_enable(this->get_pin_num());
}
size_t ESP32InternalGPIOPin::dump_summary(char *buffer, size_t len) const {

View File

@@ -19,7 +19,16 @@ static constexpr size_t KEY_BUFFER_SIZE = 12;
struct NVSData {
uint32_t key;
SmallInlineBuffer<8> data; // Most prefs fit in 8 bytes (covers fan, cover, select, etc.)
std::unique_ptr<uint8_t[]> data;
size_t len;
void set_data(const uint8_t *src, size_t size) {
if (!this->data || this->len != size) {
this->data = std::make_unique<uint8_t[]>(size);
this->len = size;
}
memcpy(this->data.get(), src, size);
}
};
static std::vector<NVSData> s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
@@ -32,14 +41,14 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend {
// try find in pending saves and update that
for (auto &obj : s_pending_save) {
if (obj.key == this->key) {
obj.data.set(data, len);
obj.set_data(data, len);
return true;
}
}
NVSData save{};
save.key = this->key;
save.data.set(data, len);
s_pending_save.push_back(std::move(save));
save.set_data(data, len);
s_pending_save.emplace_back(std::move(save));
ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len);
return true;
}
@@ -47,11 +56,11 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend {
// try find in pending saves and load from that
for (auto &obj : s_pending_save) {
if (obj.key == this->key) {
if (obj.data.size() != len) {
if (obj.len != len) {
// size mismatch
return false;
}
memcpy(data, obj.data.data(), len);
memcpy(data, obj.data.get(), len);
return true;
}
}
@@ -127,10 +136,10 @@ class ESP32Preferences : public ESPPreferences {
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str);
if (this->is_changed_(this->nvs_handle, save, key_str)) {
esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.data(), save.data.size());
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size());
esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.get(), save.len);
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.len);
if (err != 0) {
ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.data.size(), esp_err_to_name(err));
ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.len, esp_err_to_name(err));
failed++;
last_err = err;
last_key = save.key;
@@ -138,7 +147,7 @@ class ESP32Preferences : public ESPPreferences {
}
written++;
} else {
ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.data.size());
ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.len);
cached++;
}
s_pending_save.erase(s_pending_save.begin() + i);
@@ -169,7 +178,7 @@ class ESP32Preferences : public ESPPreferences {
return true;
}
// Check size first before allocating memory
if (actual_len != to_save.data.size()) {
if (actual_len != to_save.len) {
return true;
}
auto stored_data = std::make_unique<uint8_t[]>(actual_len);
@@ -178,7 +187,7 @@ class ESP32Preferences : public ESPPreferences {
ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err));
return true;
}
return memcmp(to_save.data.data(), stored_data.get(), to_save.data.size()) != 0;
return memcmp(to_save.data.get(), stored_data.get(), to_save.len) != 0;
}
bool reset() override {

View File

@@ -98,10 +98,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

@@ -46,6 +46,8 @@ class ESPBTUUID {
esp_bt_uuid_t get_uuid() const;
// Remove before 2026.8.0
ESPDEPRECATED("Use to_str() instead. Removed in 2026.8.0", "2026.2.0")
std::string to_string() const;
const char *to_str(std::span<char, UUID_STR_LEN> output) const;

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

@@ -6,7 +6,11 @@
#include "esphome/core/helpers.h"
#include "preferences.h"
#include <Arduino.h>
#include <Esp.h>
#include <core_esp8266_features.h>
extern "C" {
#include <user_interface.h>
}
namespace esphome {
@@ -16,23 +20,19 @@ void IRAM_ATTR HOT delay(uint32_t ms) { ::delay(ms); }
uint32_t IRAM_ATTR HOT micros() { return ::micros(); }
void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); }
void arch_restart() {
ESP.restart(); // NOLINT(readability-static-accessed-through-instance)
system_restart();
// restart() doesn't always end execution
while (true) { // NOLINT(clang-diagnostic-unreachable-code)
yield();
}
}
void arch_init() {}
void IRAM_ATTR HOT arch_feed_wdt() {
ESP.wdtFeed(); // NOLINT(readability-static-accessed-through-instance)
}
void IRAM_ATTR HOT arch_feed_wdt() { system_soft_wdt_feed(); }
uint8_t progmem_read_byte(const uint8_t *addr) {
return pgm_read_byte(addr); // NOLINT
}
uint32_t IRAM_ATTR HOT arch_get_cpu_cycle_count() {
return ESP.getCycleCount(); // NOLINT(readability-static-accessed-through-instance)
}
uint32_t IRAM_ATTR HOT arch_get_cpu_cycle_count() { return esp_get_cycle_count(); }
uint32_t arch_get_cpu_freq_hz() { return F_CPU; }
void force_link_symbols() {

View File

@@ -802,8 +802,8 @@ void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) {
ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed");
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
char hex_buf[format_hex_pretty_size(PHY_REG_SIZE)];
ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE));
#endif
ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE));
/*
* Bit 7 is `RMII Reference Clock Select`. Default is `0`.
@@ -820,10 +820,8 @@ void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) {
ESPHL_ERROR_CHECK(err, "Write PHY Control 2 failed");
err = mac->read_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, &(phy_control_2));
ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed");
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s",
format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE));
#endif
}
}
#endif // USE_ETHERNET_KSZ8081

View File

@@ -404,8 +404,7 @@ void EzoPMP::send_next_command_() {
break;
case EZO_PMP_COMMAND_EXEC_ARBITRARY_COMMAND_ADDRESS: // Run an arbitrary command
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), this->arbitrary_command_,
this->next_command_duration_);
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "%s", this->arbitrary_command_);
ESP_LOGI(TAG, "Sending arbitrary command: %s", (char *) command_buffer);
break;

View File

@@ -71,7 +71,7 @@ void FanCall::validate_() {
auto traits = this->parent_.get_traits();
if (this->speed_.has_value()) {
this->speed_ = clamp(*this->speed_, 1, static_cast<int>(traits.supported_speed_count()));
this->speed_ = clamp(*this->speed_, 1, traits.supported_speed_count());
// https://developers.home-assistant.io/docs/core/entity/fan/#preset-modes
// "Manually setting a speed must disable any set preset mode"

View File

@@ -11,7 +11,7 @@ namespace fan {
class FanTraits {
public:
FanTraits() = default;
FanTraits(bool oscillation, bool speed, bool direction, uint8_t speed_count)
FanTraits(bool oscillation, bool speed, bool direction, int speed_count)
: oscillation_(oscillation), speed_(speed), direction_(direction), speed_count_(speed_count) {}
/// Return if this fan supports oscillation.
@@ -23,9 +23,9 @@ class FanTraits {
/// Set whether this fan supports speed levels.
void set_speed(bool speed) { this->speed_ = speed; }
/// Return how many speed levels the fan has
uint8_t supported_speed_count() const { return this->speed_count_; }
int supported_speed_count() const { return this->speed_count_; }
/// Set how many speed levels this fan has.
void set_supported_speed_count(uint8_t speed_count) { this->speed_count_ = speed_count; }
void set_supported_speed_count(int speed_count) { this->speed_count_ = speed_count; }
/// Return if this fan supports changing direction
bool supports_direction() const { return this->direction_; }
/// Set whether this fan supports changing direction
@@ -64,7 +64,7 @@ class FanTraits {
bool oscillation_{false};
bool speed_{false};
bool direction_{false};
uint8_t speed_count_{};
int speed_count_{};
std::vector<const char *> preset_modes_{};
};

View File

@@ -39,7 +39,7 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_DECAY_MODE, default="SLOW"): cv.enum(
DECAY_MODE_OPTIONS, upper=True
),
cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1, max=255),
cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1),
cv.Optional(CONF_ENABLE_PIN): cv.use_id(output.FloatOutput),
cv.Optional(CONF_PRESET_MODES): validate_preset_modes,
}

View File

@@ -15,7 +15,7 @@ enum DecayMode {
class HBridgeFan : public Component, public fan::Fan {
public:
HBridgeFan(uint8_t speed_count, DecayMode decay_mode) : speed_count_(speed_count), decay_mode_(decay_mode) {}
HBridgeFan(int speed_count, DecayMode decay_mode) : speed_count_(speed_count), decay_mode_(decay_mode) {}
void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; }
void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; }
@@ -33,7 +33,7 @@ class HBridgeFan : public Component, public fan::Fan {
output::FloatOutput *pin_b_;
output::FloatOutput *enable_{nullptr};
output::BinaryOutput *oscillating_{nullptr};
uint8_t speed_count_{};
int speed_count_{};
DecayMode decay_mode_{DECAY_MODE_SLOW};
fan::FanTraits traits_;
std::vector<const char *> preset_modes_{};

View File

@@ -42,8 +42,8 @@ ErrorCode I2CDevice::read_register16(uint16_t a_register, uint8_t *data, size_t
}
ErrorCode I2CDevice::write_register(uint8_t a_register, const uint8_t *data, size_t len) const {
SmallBufferWithHeapFallback<17> buffer_alloc; // Most I2C writes are <= 16 bytes
uint8_t *buffer = buffer_alloc.get(len + 1);
SmallBufferWithHeapFallback<17> buffer_alloc(len + 1); // Most I2C writes are <= 16 bytes
uint8_t *buffer = buffer_alloc.get();
buffer[0] = a_register;
std::copy(data, data + len, buffer + 1);
@@ -51,8 +51,8 @@ ErrorCode I2CDevice::write_register(uint8_t a_register, const uint8_t *data, siz
}
ErrorCode I2CDevice::write_register16(uint16_t a_register, const uint8_t *data, size_t len) const {
SmallBufferWithHeapFallback<18> buffer_alloc; // Most I2C writes are <= 16 bytes + 2 for register
uint8_t *buffer = buffer_alloc.get(len + 2);
SmallBufferWithHeapFallback<18> buffer_alloc(len + 2); // Most I2C writes are <= 16 bytes + 2 for register
uint8_t *buffer = buffer_alloc.get();
buffer[0] = a_register >> 8;
buffer[1] = a_register;

View File

@@ -11,22 +11,6 @@
namespace esphome {
namespace i2c {
/// @brief Helper class for efficient buffer allocation - uses stack for small sizes, heap for large
template<size_t STACK_SIZE> class SmallBufferWithHeapFallback {
public:
uint8_t *get(size_t size) {
if (size <= STACK_SIZE) {
return this->stack_buffer_;
}
this->heap_buffer_ = std::unique_ptr<uint8_t[]>(new uint8_t[size]);
return this->heap_buffer_.get();
}
private:
uint8_t stack_buffer_[STACK_SIZE];
std::unique_ptr<uint8_t[]> heap_buffer_;
};
/// @brief Error codes returned by I2CBus and I2CDevice methods
enum ErrorCode {
NO_ERROR = 0, ///< No error found during execution of method
@@ -92,8 +76,8 @@ class I2CBus {
total_len += read_buffers[i].len;
}
SmallBufferWithHeapFallback<128> buffer_alloc; // Most I2C reads are small
uint8_t *buffer = buffer_alloc.get(total_len);
SmallBufferWithHeapFallback<128> buffer_alloc(total_len); // Most I2C reads are small
uint8_t *buffer = buffer_alloc.get();
auto err = this->write_readv(address, nullptr, 0, buffer, total_len);
if (err != ERROR_OK)
@@ -116,8 +100,8 @@ class I2CBus {
total_len += write_buffers[i].len;
}
SmallBufferWithHeapFallback<128> buffer_alloc; // Most I2C writes are small
uint8_t *buffer = buffer_alloc.get(total_len);
SmallBufferWithHeapFallback<128> buffer_alloc(total_len); // Most I2C writes are small
uint8_t *buffer = buffer_alloc.get();
size_t pos = 0;
for (size_t i = 0; i != count; i++) {

View File

@@ -119,7 +119,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

@@ -1,11 +1,6 @@
from esphome import pins
import esphome.codegen as cg
from esphome.components.esp32 import (
add_idf_sdkconfig_option,
enable_ringbuf_in_iram,
get_esp32_variant,
)
from esphome.components.esp32.const import (
VARIANT_ESP32,
VARIANT_ESP32C3,
VARIANT_ESP32C5,
@@ -15,6 +10,8 @@ from esphome.components.esp32.const import (
VARIANT_ESP32P4,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
add_idf_sdkconfig_option,
get_esp32_variant,
)
import esphome.config_validation as cv
from esphome.const import CONF_BITS_PER_SAMPLE, CONF_CHANNEL, CONF_ID, CONF_SAMPLE_RATE
@@ -281,9 +278,6 @@ async def to_code(config):
# Helps avoid callbacks being skipped due to processor load
add_idf_sdkconfig_option("CONFIG_I2S_ISR_IRAM_SAFE", True)
# Keep ring buffer functions in IRAM for audio performance
enable_ringbuf_in_iram()
cg.add(var.set_lrclk_pin(config[CONF_I2S_LRCLK_PIN]))
if CONF_I2S_BCLK_PIN in config:
cg.add(var.set_bclk_pin(config[CONF_I2S_BCLK_PIN]))

View File

@@ -11,7 +11,7 @@ static const char *const TAG = "kuntze";
static const uint8_t CMD_READ_REG = 0x03;
static const uint16_t REGISTER[] = {4136, 4160, 4680, 6000, 4688, 4728, 5832};
// Maximum bytes to log for Modbus responses (2 registers = 4 bytes, plus byte count = 5 bytes)
// Maximum bytes to log for Modbus responses (2 registers = 4, plus count = 5)
static constexpr size_t KUNTZE_MAX_LOG_BYTES = 8;
void Kuntze::on_modbus_data(const std::vector<uint8_t> &data) {

View File

@@ -18,7 +18,16 @@ static constexpr size_t KEY_BUFFER_SIZE = 12;
struct NVSData {
uint32_t key;
SmallInlineBuffer<8> data; // Most prefs fit in 8 bytes (covers fan, cover, select, etc.)
std::unique_ptr<uint8_t[]> data;
size_t len;
void set_data(const uint8_t *src, size_t size) {
if (!this->data || this->len != size) {
this->data = std::make_unique<uint8_t[]>(size);
this->len = size;
}
memcpy(this->data.get(), src, size);
}
};
static std::vector<NVSData> s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
@@ -33,14 +42,14 @@ class LibreTinyPreferenceBackend : public ESPPreferenceBackend {
// try find in pending saves and update that
for (auto &obj : s_pending_save) {
if (obj.key == this->key) {
obj.data.set(data, len);
obj.set_data(data, len);
return true;
}
}
NVSData save{};
save.key = this->key;
save.data.set(data, len);
s_pending_save.push_back(std::move(save));
save.set_data(data, len);
s_pending_save.emplace_back(std::move(save));
ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len);
return true;
}
@@ -49,11 +58,11 @@ class LibreTinyPreferenceBackend : public ESPPreferenceBackend {
// try find in pending saves and load from that
for (auto &obj : s_pending_save) {
if (obj.key == this->key) {
if (obj.data.size() != len) {
if (obj.len != len) {
// size mismatch
return false;
}
memcpy(data, obj.data.data(), len);
memcpy(data, obj.data.get(), len);
return true;
}
}
@@ -117,11 +126,11 @@ class LibreTinyPreferences : public ESPPreferences {
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
ESP_LOGVV(TAG, "Checking if FDB data %s has changed", key_str);
if (this->is_changed_(&this->db, save, key_str)) {
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size());
fdb_blob_make(&this->blob, save.data.data(), save.data.size());
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.len);
fdb_blob_make(&this->blob, save.data.get(), save.len);
fdb_err_t err = fdb_kv_set_blob(&this->db, key_str, &this->blob);
if (err != FDB_NO_ERR) {
ESP_LOGV(TAG, "fdb_kv_set_blob('%s', len=%zu) failed: %d", key_str, save.data.size(), err);
ESP_LOGV(TAG, "fdb_kv_set_blob('%s', len=%zu) failed: %d", key_str, save.len, err);
failed++;
last_err = err;
last_key = save.key;
@@ -129,7 +138,7 @@ class LibreTinyPreferences : public ESPPreferences {
}
written++;
} else {
ESP_LOGD(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.data.size());
ESP_LOGD(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.len);
cached++;
}
s_pending_save.erase(s_pending_save.begin() + i);
@@ -153,7 +162,7 @@ class LibreTinyPreferences : public ESPPreferences {
}
// Check size first - if different, data has changed
if (kv.value_len != to_save.data.size()) {
if (kv.value_len != to_save.len) {
return true;
}
@@ -167,7 +176,7 @@ class LibreTinyPreferences : public ESPPreferences {
}
// Compare the actual data
return memcmp(to_save.data.data(), stored_data.get(), kv.value_len) != 0;
return memcmp(to_save.data.get(), stored_data.get(), kv.value_len) != 0;
}
bool reset() override {

View File

@@ -28,16 +28,14 @@ const LogString *lock_state_to_string(LockState state) {
Lock::Lock() : state(LOCK_STATE_NONE) {}
LockCall Lock::make_call() { return LockCall(this); }
void Lock::lock() {
void Lock::set_state_(LockState state) {
auto call = this->make_call();
call.set_state(LOCK_STATE_LOCKED);
this->control(call);
}
void Lock::unlock() {
auto call = this->make_call();
call.set_state(LOCK_STATE_UNLOCKED);
call.set_state(state);
this->control(call);
}
void Lock::lock() { this->set_state_(LOCK_STATE_LOCKED); }
void Lock::unlock() { this->set_state_(LOCK_STATE_UNLOCKED); }
void Lock::open() {
if (traits.get_supports_open()) {
ESP_LOGD(TAG, "'%s' Opening.", this->get_name().c_str());

View File

@@ -156,6 +156,9 @@ class Lock : public EntityBase {
protected:
friend LockCall;
/// Helper for lock/unlock convenience methods
void set_state_(LockState state);
/** Perform the open latch action with hardware. This method is optional to implement
* when creating a new lock.
*

View File

@@ -1,51 +0,0 @@
#ifdef USE_ESP8266
#include "logger.h"
#include "esphome/core/log.h"
namespace esphome::logger {
static const char *const TAG = "logger";
void Logger::pre_setup() {
if (this->baud_rate_ > 0) {
switch (this->uart_) {
case UART_SELECTION_UART0:
case UART_SELECTION_UART0_SWAP:
this->hw_serial_ = &Serial;
Serial.begin(this->baud_rate_);
if (this->uart_ == UART_SELECTION_UART0_SWAP) {
Serial.swap();
}
Serial.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE);
break;
case UART_SELECTION_UART1:
this->hw_serial_ = &Serial1;
Serial1.begin(this->baud_rate_);
Serial1.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE);
break;
}
} else {
uart_set_debug(UART_NO);
}
global_logger = this;
ESP_LOGI(TAG, "Log initialized");
}
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
const LogString *Logger::get_uart_selection_() {
switch (this->uart_) {
case UART_SELECTION_UART0:
return LOG_STR("UART0");
case UART_SELECTION_UART1:
return LOG_STR("UART1");
case UART_SELECTION_UART0_SWAP:
default:
return LOG_STR("UART0_SWAP");
}
}
} // namespace esphome::logger
#endif

View File

@@ -1,22 +0,0 @@
#if defined(USE_HOST)
#include "logger.h"
namespace esphome::logger {
void HOT Logger::write_msg_(const char *msg) {
time_t rawtime;
struct tm *timeinfo;
char buffer[80];
time(&rawtime);
timeinfo = localtime(&rawtime);
strftime(buffer, sizeof buffer, "[%H:%M:%S]", timeinfo);
fputs(buffer, stdout);
puts(msg);
}
void Logger::pre_setup() { global_logger = this; }
} // namespace esphome::logger
#endif

View File

@@ -1,70 +0,0 @@
#ifdef USE_LIBRETINY
#include "logger.h"
namespace esphome::logger {
static const char *const TAG = "logger";
void Logger::pre_setup() {
if (this->baud_rate_ > 0) {
switch (this->uart_) {
#if LT_HW_UART0
case UART_SELECTION_UART0:
this->hw_serial_ = &Serial0;
Serial0.begin(this->baud_rate_);
break;
#endif
#if LT_HW_UART1
case UART_SELECTION_UART1:
this->hw_serial_ = &Serial1;
Serial1.begin(this->baud_rate_);
break;
#endif
#if LT_HW_UART2
case UART_SELECTION_UART2:
this->hw_serial_ = &Serial2;
Serial2.begin(this->baud_rate_);
break;
#endif
default:
this->hw_serial_ = &Serial;
Serial.begin(this->baud_rate_);
if (this->uart_ != UART_SELECTION_DEFAULT) {
ESP_LOGW(TAG, " The chosen logger UART port is not available on this board."
"The default port was used instead.");
}
break;
}
// change lt_log() port to match default Serial
if (this->uart_ == UART_SELECTION_DEFAULT) {
this->uart_ = (UARTSelection) (LT_UART_DEFAULT_SERIAL + 1);
lt_log_set_port(LT_UART_DEFAULT_SERIAL);
} else {
lt_log_set_port(this->uart_ - 1);
}
}
global_logger = this;
ESP_LOGI(TAG, "Log initialized");
}
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
const LogString *Logger::get_uart_selection_() {
switch (this->uart_) {
case UART_SELECTION_DEFAULT:
return LOG_STR("DEFAULT");
case UART_SELECTION_UART0:
return LOG_STR("UART0");
case UART_SELECTION_UART1:
return LOG_STR("UART1");
case UART_SELECTION_UART2:
default:
return LOG_STR("UART2");
}
}
} // namespace esphome::logger
#endif // USE_LIBRETINY

View File

@@ -1,48 +0,0 @@
#ifdef USE_RP2040
#include "logger.h"
#include "esphome/core/log.h"
namespace esphome::logger {
static const char *const TAG = "logger";
void Logger::pre_setup() {
if (this->baud_rate_ > 0) {
switch (this->uart_) {
case UART_SELECTION_UART0:
this->hw_serial_ = &Serial1;
Serial1.begin(this->baud_rate_);
break;
case UART_SELECTION_UART1:
this->hw_serial_ = &Serial2;
Serial2.begin(this->baud_rate_);
break;
case UART_SELECTION_USB_CDC:
this->hw_serial_ = &Serial;
Serial.begin(this->baud_rate_);
break;
}
}
global_logger = this;
ESP_LOGI(TAG, "Log initialized");
}
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
const LogString *Logger::get_uart_selection_() {
switch (this->uart_) {
case UART_SELECTION_UART0:
return LOG_STR("UART0");
case UART_SELECTION_UART1:
return LOG_STR("UART1");
#ifdef USE_LOGGER_USB_CDC
case UART_SELECTION_USB_CDC:
return LOG_STR("USB_CDC");
#endif
default:
return LOG_STR("UNKNOWN");
}
}
} // namespace esphome::logger
#endif // USE_RP2040

View File

@@ -1,96 +0,0 @@
#ifdef USE_ZEPHYR
#include "esphome/core/application.h"
#include "esphome/core/log.h"
#include "logger.h"
#include <zephyr/device.h>
#include <zephyr/drivers/uart.h>
#include <zephyr/usb/usb_device.h>
namespace esphome::logger {
static const char *const TAG = "logger";
#ifdef USE_LOGGER_USB_CDC
void Logger::loop() {
if (this->uart_ != UART_SELECTION_USB_CDC || nullptr == this->uart_dev_) {
return;
}
static bool opened = false;
uint32_t dtr = 0;
uart_line_ctrl_get(this->uart_dev_, UART_LINE_CTRL_DTR, &dtr);
/* Poll if the DTR flag was set, optional */
if (opened == dtr) {
return;
}
if (!opened) {
App.schedule_dump_config();
}
opened = !opened;
}
#endif
void Logger::pre_setup() {
if (this->baud_rate_ > 0) {
static const struct device *uart_dev = nullptr;
switch (this->uart_) {
case UART_SELECTION_UART0:
uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(uart0));
break;
case UART_SELECTION_UART1:
uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(uart1));
break;
#ifdef USE_LOGGER_USB_CDC
case UART_SELECTION_USB_CDC:
uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(cdc_acm_uart0));
if (device_is_ready(uart_dev)) {
usb_enable(nullptr);
}
break;
#endif
}
if (!device_is_ready(uart_dev)) {
ESP_LOGE(TAG, "%s is not ready.", LOG_STR_ARG(get_uart_selection_()));
} else {
this->uart_dev_ = uart_dev;
}
}
global_logger = this;
ESP_LOGI(TAG, "Log initialized");
}
void HOT Logger::write_msg_(const char *msg) {
#ifdef CONFIG_PRINTK
printk("%s\n", msg);
#endif
if (nullptr == this->uart_dev_) {
return;
}
while (*msg) {
uart_poll_out(this->uart_dev_, *msg);
++msg;
}
uart_poll_out(this->uart_dev_, '\n');
}
const LogString *Logger::get_uart_selection_() {
switch (this->uart_) {
case UART_SELECTION_UART0:
return LOG_STR("UART0");
case UART_SELECTION_UART1:
return LOG_STR("UART1");
#ifdef USE_LOGGER_USB_CDC
case UART_SELECTION_USB_CDC:
return LOG_STR("USB_CDC");
#endif
default:
return LOG_STR("UNKNOWN");
}
}
} // namespace esphome::logger
#endif

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

@@ -448,9 +448,6 @@ async def to_code(config):
# The inference task queues detection events that need immediate processing
socket.require_wake_loop_threadsafe()
# Keep ring buffer functions in IRAM for audio performance
esp32.enable_ringbuf_in_iram()
mic_source = await microphone.microphone_source_to_code(config[CONF_MICROPHONE])
cg.add(var.set_microphone_source(mic_source))

View File

@@ -224,12 +224,9 @@ class MipiSpi : public display::Display,
this->madctl_ & MADCTL_BGR ? "BGR" : "RGB", DISPLAYPIXEL * 8, IS_BIG_ENDIAN ? "Big" : "Little");
if (this->brightness_.has_value())
esph_log_config(TAG, " Brightness: %u", this->brightness_.value());
if (this->cs_ != nullptr)
esph_log_config(TAG, " CS Pin: %s", this->cs_->dump_summary().c_str());
if (this->reset_pin_ != nullptr)
esph_log_config(TAG, " Reset Pin: %s", this->reset_pin_->dump_summary().c_str());
if (this->dc_pin_ != nullptr)
esph_log_config(TAG, " DC Pin: %s", this->dc_pin_->dump_summary().c_str());
log_pin(TAG, " CS Pin: ", this->cs_);
log_pin(TAG, " Reset Pin: ", this->reset_pin_);
log_pin(TAG, " DC Pin: ", this->dc_pin_);
esph_log_config(TAG,
" SPI Mode: %d\n"
" SPI Data rate: %dMHz\n"

View File

@@ -26,5 +26,3 @@ ST7789V.extend(
reset_pin=40,
invert_colors=True,
)
models = {}

View File

@@ -105,6 +105,3 @@ CO5300 = DriverChip(
(WCE, 0x00),
),
)
models = {}

View File

@@ -1,10 +1,45 @@
from .ili import ILI9341
from .ili import ILI9341, ILI9342, ST7789V
ILI9341.extend(
# ESP32-2432S028 CYD board with Micro USB, has ILI9341 controller
"ESP32-2432S028",
data_rate="40MHz",
cs_pin=15,
dc_pin=2,
cs_pin={"number": 15, "ignore_strapping_warning": True},
dc_pin={"number": 2, "ignore_strapping_warning": True},
)
models = {}
ST7789V.extend(
# ESP32-2432S028 CYD board with USB C + Micro USB, has ST7789V controller
"ESP32-2432S028-7789",
data_rate="40MHz",
cs_pin={"number": 15, "ignore_strapping_warning": True},
dc_pin={"number": 2, "ignore_strapping_warning": True},
)
# fmt: off
ILI9342.extend(
# ESP32-2432S028 CYD board with USB C + Micro USB, has ILI9342 controller
"ESP32-2432S028-9342",
data_rate="40MHz",
cs_pin={"number": 15, "ignore_strapping_warning": True},
dc_pin={"number": 2, "ignore_strapping_warning": True},
initsequence=(
(0xCB, 0x39, 0x2C, 0x00, 0x34, 0x02), # Power Control A
(0xCF, 0x00, 0xC1, 0x30), # Power Control B
(0xE8, 0x85, 0x00, 0x78), # Driver timing control A
(0xEA, 0x00, 0x00), # Driver timing control B
(0xED, 0x64, 0x03, 0x12, 0x81), # Power on sequence control
(0xF7, 0x20), # Pump ratio control
(0xC0, 0x23), # Power Control 1
(0xC1, 0x10), # Power Control 2
(0xC5, 0x3E, 0x28), # VCOM Control 1
(0xC7, 0x86), # VCOM Control 2
(0xB1, 0x00, 0x1B), # Frame Rate Control
(0xB6, 0x0A, 0xA2, 0x27, 0x00), # Display Function Control
(0xF2, 0x00), # Enable 3G
(0x26, 0x01), # Gamma Set
(0xE0, 0x00, 0x0C, 0x11, 0x04, 0x11, 0x08, 0x37, 0x89, 0x4C, 0x06, 0x0C, 0x0A, 0x2E, 0x34, 0x0F), # Positive Gamma Correction
(0xE1, 0x00, 0x0B, 0x11, 0x05, 0x13, 0x09, 0x33, 0x67, 0x48, 0x07, 0x0E, 0x0B, 0x23, 0x33, 0x0F), # Negative Gamma Correction
)
)

View File

@@ -148,6 +148,34 @@ ILI9341 = DriverChip(
),
),
)
# fmt: off
ILI9342 = DriverChip(
"ILI9342",
width=320,
height=240,
mirror_x=True,
initsequence=(
(0xCB, 0x39, 0x2C, 0x00, 0x34, 0x02), # Power Control A
(0xCF, 0x00, 0xC1, 0x30), # Power Control B
(0xE8, 0x85, 0x00, 0x78), # Driver timing control A
(0xEA, 0x00, 0x00), # Driver timing control B
(0xED, 0x64, 0x03, 0x12, 0x81), # Power on sequence control
(0xF7, 0x20), # Pump ratio control
(0xC0, 0x23), # Power Control 1
(0xC1, 0x10), # Power Control 2
(0xC5, 0x3E, 0x28), # VCOM Control 1
(0xC7, 0x86), # VCOM Control 2
(0xB1, 0x00, 0x1B), # Frame Rate Control
(0xB6, 0x0A, 0xA2, 0x27, 0x00), # Display Function Control
(0xF2, 0x00), # Enable 3G
(0x26, 0x01), # Gamma Set
(0xE0, 0x0F, 0x1F, 0x1C, 0x0C, 0x0F, 0x08, 0x48, 0x98, 0x37, 0x0A, 0x13, 0x04, 0x11, 0x0D, 0x00), # Positive Gamma
(0xE1, 0x0F, 0x32, 0x2E, 0x0B, 0x0D, 0x05, 0x47, 0x75, 0x37, 0x06, 0x10, 0x03, 0x24, 0x20, 0x00), # Negative Gamma
),
)
# M5Stack Core2 uses ILI9341 chip - mirror_x disabled for correct orientation
ILI9341.extend(
"M5CORE2",
@@ -758,5 +786,3 @@ ST7796.extend(
dc_pin=0,
invert_colors=True,
)
models = {}

View File

@@ -588,5 +588,3 @@ DriverChip(
(0x29, 0x00),
),
)
models = {}

View File

@@ -11,5 +11,3 @@ ST7789V.extend(
dc_pin=21,
reset_pin=18,
)
models = {}

View File

@@ -56,5 +56,3 @@ ST7796.extend(
backlight_pin=48,
invert_colors=True,
)
models = {}

View File

@@ -271,9 +271,9 @@ class ServerRegister {
// Formats a raw value into a string representation based on the value type for debugging
std::string format_value(int64_t value) const {
// max 48: float with %.1f can be up to 42 chars (3.4e38 → 38 integer digits + decimal + 1 digit + sign + null)
// int64_t max is 20 chars + sign + null = 22, so 48 covers both
char buf[48];
// max 44: float with %.1f can be up to 42 chars (3.4e38 → 39 integer digits + sign + decimal + 1 digit)
// plus null terminator = 43, rounded to 44 for 4-byte alignment
char buf[44];
switch (this->value_type) {
case SensorValueType::U_WORD:
case SensorValueType::U_DWORD:

View File

@@ -18,7 +18,7 @@ bool CustomMQTTDevice::publish(const std::string &topic, float value, int8_t num
}
bool CustomMQTTDevice::publish(const std::string &topic, int value) {
char buffer[24];
size_t len = buf_append_printf(buffer, sizeof(buffer), 0, "%d", value);
int len = snprintf(buffer, sizeof(buffer), "%d", value);
return global_mqtt_client->publish(topic, buffer, len);
}
bool CustomMQTTDevice::publish_json(const std::string &topic, const json::json_build_t &f, uint8_t qos, bool retain) {

View File

@@ -5,6 +5,7 @@
#include <utility>
#include "esphome/components/network/util.h"
#include "esphome/core/application.h"
#include "esphome/core/entity_base.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/version.h"
@@ -66,10 +67,13 @@ void MQTTClientComponent::setup() {
"esphome/discover", [this](const std::string &topic, const std::string &payload) { this->send_device_info_(); },
2);
std::string topic = "esphome/ping/";
topic.append(App.get_name());
// Format topic on stack - subscribe() copies it
// "esphome/ping/" (13) + name (ESPHOME_DEVICE_NAME_MAX_LEN) + null (1)
constexpr size_t ping_topic_buffer_size = 13 + ESPHOME_DEVICE_NAME_MAX_LEN + 1;
char ping_topic[ping_topic_buffer_size];
buf_append_printf(ping_topic, sizeof(ping_topic), 0, "esphome/ping/%s", App.get_name().c_str());
this->subscribe(
topic, [this](const std::string &topic, const std::string &payload) { this->send_device_info_(); }, 2);
ping_topic, [this](const std::string &topic, const std::string &payload) { this->send_device_info_(); }, 2);
}
if (this->enable_on_boot_) {
@@ -81,8 +85,11 @@ void MQTTClientComponent::send_device_info_() {
if (!this->is_connected() or !this->is_discovery_ip_enabled()) {
return;
}
std::string topic = "esphome/discover/";
topic.append(App.get_name());
// Format topic on stack to avoid heap allocation
// "esphome/discover/" (17) + name (ESPHOME_DEVICE_NAME_MAX_LEN) + null (1)
constexpr size_t topic_buffer_size = 17 + ESPHOME_DEVICE_NAME_MAX_LEN + 1;
char topic[topic_buffer_size];
buf_append_printf(topic, sizeof(topic), 0, "esphome/discover/%s", App.get_name().c_str());
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
this->publish_json(
@@ -91,17 +98,7 @@ void MQTTClientComponent::send_device_info_() {
uint8_t index = 0;
for (auto &ip : network::get_ip_addresses()) {
if (ip.is_set()) {
char key[8]; // "ip" + up to 3 digits + null
char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
if (index == 0) {
key[0] = 'i';
key[1] = 'p';
key[2] = '\0';
} else {
buf_append_printf(key, sizeof(key), 0, "ip%u", index);
}
ip.str_to(ip_buf);
root[key] = ip_buf;
root["ip" + (index == 0 ? "" : esphome::to_string(index))] = ip.str();
index++;
}
}
@@ -406,6 +403,12 @@ void MQTTClientComponent::loop() {
this->last_connected_ = now;
this->resubscribe_subscriptions_();
// Process pending resends for all MQTT components centrally
// This is more efficient than each component polling in its own loop
for (MQTTComponent *component : this->children_) {
component->process_resend();
}
}
break;
}
@@ -510,39 +513,49 @@ bool MQTTClientComponent::publish(const std::string &topic, const std::string &p
bool MQTTClientComponent::publish(const std::string &topic, const char *payload, size_t payload_length, uint8_t qos,
bool retain) {
return publish({.topic = topic, .payload = std::string(payload, payload_length), .qos = qos, .retain = retain});
return this->publish(topic.c_str(), payload, payload_length, qos, retain);
}
bool MQTTClientComponent::publish(const MQTTMessage &message) {
return this->publish(message.topic.c_str(), message.payload.c_str(), message.payload.length(), message.qos,
message.retain);
}
bool MQTTClientComponent::publish_json(const std::string &topic, const json::json_build_t &f, uint8_t qos,
bool retain) {
return this->publish_json(topic.c_str(), f, qos, retain);
}
bool MQTTClientComponent::publish(const char *topic, const char *payload, size_t payload_length, uint8_t qos,
bool retain) {
if (!this->is_connected()) {
// critical components will re-transmit their messages
return false;
}
bool logging_topic = this->log_message_.topic == message.topic;
bool ret = this->mqtt_backend_.publish(message);
size_t topic_len = strlen(topic);
bool logging_topic = (topic_len == this->log_message_.topic.size()) &&
(memcmp(this->log_message_.topic.c_str(), topic, topic_len) == 0);
bool ret = this->mqtt_backend_.publish(topic, payload, payload_length, qos, retain);
delay(0);
if (!ret && !logging_topic && this->is_connected()) {
delay(0);
ret = this->mqtt_backend_.publish(message);
ret = this->mqtt_backend_.publish(topic, payload, payload_length, qos, retain);
delay(0);
}
if (!logging_topic) {
if (ret) {
ESP_LOGV(TAG, "Publish(topic='%s' payload='%s' retain=%d qos=%d)", message.topic.c_str(), message.payload.c_str(),
message.retain, message.qos);
ESP_LOGV(TAG, "Publish(topic='%s' retain=%d qos=%d)", topic, retain, qos);
ESP_LOGVV(TAG, "Publish payload (len=%u): '%.*s'", payload_length, static_cast<int>(payload_length), payload);
} else {
ESP_LOGV(TAG, "Publish failed for topic='%s' (len=%u). Will retry", message.topic.c_str(),
message.payload.length());
ESP_LOGV(TAG, "Publish failed for topic='%s' (len=%u). Will retry", topic, payload_length);
this->status_momentary_warning("publish", 1000);
}
}
return ret != 0;
}
bool MQTTClientComponent::publish_json(const std::string &topic, const json::json_build_t &f, uint8_t qos,
bool retain) {
bool MQTTClientComponent::publish_json(const char *topic, const json::json_build_t &f, uint8_t qos, bool retain) {
std::string message = json::build_json(f);
return this->publish(topic, message, qos, retain);
return this->publish(topic, message.c_str(), message.length(), qos, retain);
}
void MQTTClientComponent::enable() {
@@ -620,18 +633,10 @@ static bool topic_match(const char *message, const char *subscription) {
}
void MQTTClientComponent::on_message(const std::string &topic, const std::string &payload) {
#ifdef USE_ESP8266
// on ESP8266, this is called in lwIP/AsyncTCP task; some components do not like running
// from a different task.
this->defer([this, topic, payload]() {
#endif
for (auto &subscription : this->subscriptions_) {
if (topic_match(topic.c_str(), subscription.topic.c_str()))
subscription.callback(topic, payload);
}
#ifdef USE_ESP8266
});
#endif
for (auto &subscription : this->subscriptions_) {
if (topic_match(topic.c_str(), subscription.topic.c_str()))
subscription.callback(topic, payload);
}
}
// Setters

View File

@@ -229,6 +229,9 @@ class MQTTClientComponent : public Component
bool publish(const std::string &topic, const char *payload, size_t payload_length, uint8_t qos = 0,
bool retain = false);
/// Publish directly without creating MQTTMessage (avoids heap allocation for topic)
bool publish(const char *topic, const char *payload, size_t payload_length, uint8_t qos = 0, bool retain = false);
/** Construct and send a JSON MQTT message.
*
* @param topic The topic.
@@ -237,6 +240,9 @@ class MQTTClientComponent : public Component
*/
bool publish_json(const std::string &topic, const json::json_build_t &f, uint8_t qos = 0, bool retain = false);
/// Publish JSON directly without heap allocation for topic
bool publish_json(const char *topic, const json::json_build_t &f, uint8_t qos = 0, bool retain = false);
/// Setup the MQTT client, registering a bunch of callbacks and attempting to connect.
void setup() override;
void dump_config() override;

View File

@@ -190,37 +190,27 @@ bool MQTTComponent::send_discovery_() {
StringRef object_id = this->get_default_object_id_to_(object_id_buf);
if (discovery_info.unique_id_generator == MQTT_MAC_ADDRESS_UNIQUE_ID_GENERATOR) {
char friendly_name_hash[9];
buf_append_printf(friendly_name_hash, sizeof(friendly_name_hash), 0, "%08" PRIx32,
fnv1_hash(this->friendly_name_()));
snprintf(friendly_name_hash, sizeof(friendly_name_hash), "%08" PRIx32, fnv1_hash(this->friendly_name_()));
// Format: mac-component_type-hash (e.g. "aabbccddeeff-sensor-12345678")
// MAC (12) + "-" (1) + domain (max 20) + "-" (1) + hash (8) + null (1) = 43
char unique_id[MAC_ADDRESS_BUFFER_SIZE + ESPHOME_DOMAIN_MAX_LEN + 11];
char mac_buf[MAC_ADDRESS_BUFFER_SIZE];
get_mac_address_into_buffer(mac_buf);
buf_append_printf(unique_id, sizeof(unique_id), 0, "%s-%s-%s", mac_buf, this->component_type(),
friendly_name_hash);
snprintf(unique_id, sizeof(unique_id), "%s-%s-%s", mac_buf, this->component_type(), friendly_name_hash);
root[MQTT_UNIQUE_ID] = unique_id;
} else {
// default to almost-unique ID. It's a hack but the only way to get that
// gorgeous device registry view.
// "ESP" (3) + component_type (max 20) + object_id (max 128) + null
char unique_id_buf[3 + MQTT_COMPONENT_TYPE_MAX_LEN + OBJECT_ID_MAX_LEN + 1];
buf_append_printf(unique_id_buf, sizeof(unique_id_buf), 0, "ESP%s%s", this->component_type(),
object_id.c_str());
root[MQTT_UNIQUE_ID] = unique_id_buf;
root[MQTT_UNIQUE_ID] = "ESP" + std::string(this->component_type()) + object_id.c_str();
}
const std::string &node_name = App.get_name();
if (discovery_info.object_id_generator == MQTT_DEVICE_NAME_OBJECT_ID_GENERATOR) {
// node_name (max 31) + "_" (1) + object_id (max 128) + null
char object_id_full[ESPHOME_DEVICE_NAME_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 1];
buf_append_printf(object_id_full, sizeof(object_id_full), 0, "%s_%s", node_name.c_str(), object_id.c_str());
root[MQTT_OBJECT_ID] = object_id_full;
}
if (discovery_info.object_id_generator == MQTT_DEVICE_NAME_OBJECT_ID_GENERATOR)
root[MQTT_OBJECT_ID] = node_name + "_" + object_id.c_str();
const std::string &friendly_name_ref = App.get_friendly_name();
const std::string &node_friendly_name = friendly_name_ref.empty() ? node_name : friendly_name_ref;
const char *node_area = App.get_area();
std::string node_area = App.get_area();
JsonObject device_info = root[MQTT_DEVICE].to<JsonObject>();
char mac[MAC_ADDRESS_BUFFER_SIZE];
@@ -231,29 +221,18 @@ bool MQTTComponent::send_discovery_() {
device_info[MQTT_DEVICE_SW_VERSION] = ESPHOME_PROJECT_VERSION " (ESPHome " ESPHOME_VERSION ")";
const char *model = std::strchr(ESPHOME_PROJECT_NAME, '.');
device_info[MQTT_DEVICE_MODEL] = model == nullptr ? ESPHOME_BOARD : model + 1;
if (model == nullptr) {
device_info[MQTT_DEVICE_MANUFACTURER] = ESPHOME_PROJECT_NAME;
} else {
// Extract manufacturer (part before '.') using stack buffer to avoid heap allocation
// memcpy is used instead of strncpy since we know the exact length and strncpy
// would still require manual null-termination
char manufacturer[sizeof(ESPHOME_PROJECT_NAME)];
size_t len = model - ESPHOME_PROJECT_NAME;
memcpy(manufacturer, ESPHOME_PROJECT_NAME, len);
manufacturer[len] = '\0';
device_info[MQTT_DEVICE_MANUFACTURER] = manufacturer;
}
device_info[MQTT_DEVICE_MANUFACTURER] =
model == nullptr ? ESPHOME_PROJECT_NAME : std::string(ESPHOME_PROJECT_NAME, model - ESPHOME_PROJECT_NAME);
#else
static const char ver_fmt[] PROGMEM = ESPHOME_VERSION " (config hash 0x%08" PRIx32 ")";
// Buffer sized for format string expansion: ~4 bytes net growth from format specifier to 8 hex digits, plus
// safety margin
char version_buf[sizeof(ver_fmt) + 8];
#ifdef USE_ESP8266
snprintf_P(version_buf, sizeof(version_buf), ver_fmt, App.get_config_hash());
char fmt_buf[sizeof(ver_fmt)];
strcpy_P(fmt_buf, ver_fmt);
const char *fmt = fmt_buf;
#else
snprintf(version_buf, sizeof(version_buf), ver_fmt, App.get_config_hash());
const char *fmt = ver_fmt;
#endif
device_info[MQTT_DEVICE_SW_VERSION] = version_buf;
device_info[MQTT_DEVICE_SW_VERSION] = str_sprintf(fmt, App.get_config_hash());
device_info[MQTT_DEVICE_MODEL] = ESPHOME_BOARD;
#if defined(USE_ESP8266) || defined(USE_ESP32)
device_info[MQTT_DEVICE_MANUFACTURER] = "Espressif";
@@ -267,7 +246,7 @@ bool MQTTComponent::send_discovery_() {
device_info[MQTT_DEVICE_MANUFACTURER] = "Host";
#endif
#endif
if (node_area[0] != '\0') {
if (!node_area.empty()) {
device_info[MQTT_DEVICE_SUGGESTED_AREA] = node_area;
}
@@ -329,16 +308,12 @@ void MQTTComponent::call_setup() {
}
}
void MQTTComponent::call_loop() {
if (this->is_internal())
void MQTTComponent::process_resend() {
// Called by MQTTClientComponent when connected to process pending resends
// Note: is_internal() check not needed - internal components are never registered
if (!this->resend_state_)
return;
this->loop();
if (!this->resend_state_ || !this->is_connected_()) {
return;
}
this->resend_state_ = false;
if (this->is_discovery_enabled()) {
if (!this->send_discovery_()) {

View File

@@ -81,8 +81,6 @@ class MQTTComponent : public Component {
/// Override setup_ so that we can call send_discovery() when needed.
void call_setup() override;
void call_loop() override;
void call_dump_config() override;
/// Send discovery info the Home Assistant, override this.
@@ -133,6 +131,9 @@ class MQTTComponent : public Component {
/// Internal method for the MQTT client base to schedule a resend of the state on reconnect.
void schedule_resend_state();
/// Process pending resend if needed (called by MQTTClientComponent)
void process_resend();
/** Send a MQTT message.
*
* @param topic The topic.

View File

@@ -175,7 +175,7 @@ bool MQTTFanComponent::publish_state() {
auto traits = this->state_->get_traits();
if (traits.supports_speed()) {
char buf[12];
size_t len = buf_append_printf(buf, sizeof(buf), 0, "%d", this->state_->speed);
int len = snprintf(buf, sizeof(buf), "%d", this->state_->speed);
bool success = this->publish(this->get_speed_level_state_topic(), buf, len);
failed = failed || !success;
}

View File

@@ -75,7 +75,7 @@ bool MQTTNumberComponent::send_initial_state() {
}
bool MQTTNumberComponent::publish_state(float value) {
char buffer[64];
buf_append_printf(buffer, sizeof(buffer), 0, "%f", value);
snprintf(buffer, sizeof(buffer), "%f", value);
return this->publish(this->get_state_topic_(), buffer);
}

View File

@@ -60,6 +60,8 @@ struct IPAddress {
}
IPAddress(const std::string &in_address) { inet_aton(in_address.c_str(), &ip_addr_); }
IPAddress(const ip_addr_t *other_ip) { ip_addr_ = *other_ip; }
// Remove before 2026.8.0
ESPDEPRECATED("Use str_to() instead. Removed in 2026.8.0", "2026.2.0")
std::string str() const {
char buf[IP_ADDRESS_BUFFER_SIZE];
this->str_to(buf);
@@ -147,6 +149,8 @@ struct IPAddress {
bool is_ip4() const { return IP_IS_V4(&ip_addr_); }
bool is_ip6() const { return IP_IS_V6(&ip_addr_); }
bool is_multicast() const { return ip_addr_ismulticast(&ip_addr_); }
// Remove before 2026.8.0
ESPDEPRECATED("Use str_to() instead. Removed in 2026.8.0", "2026.2.0")
std::string str() const {
char buf[IP_ADDRESS_BUFFER_SIZE];
this->str_to(buf);

View File

@@ -8,12 +8,12 @@ namespace pipsolar {
static const char *const TAG = "pipsolar.output";
void PipsolarOutput::write_state(float state) {
char tmp[10];
char tmp[16];
snprintf(tmp, sizeof(tmp), this->set_command_, state);
if (std::find(this->possible_values_.begin(), this->possible_values_.end(), state) != this->possible_values_.end()) {
ESP_LOGD(TAG, "Will write: %s out of value %f / %02.0f", tmp, state, state);
this->parent_->queue_command(tmp);
this->parent_->queue_command(std::string(tmp));
} else {
ESP_LOGD(TAG, "Will not write: %s as it is not in list of allowed values", tmp);
}

View File

@@ -45,20 +45,20 @@ void Pipsolar::loop() {
} else {
ESP_LOGD(TAG, "command not successful");
}
this->command_queue_[this->command_queue_position_].clear();
this->command_queue_[this->command_queue_position_] = std::string("");
this->command_queue_position_ = (command_queue_position_ + 1) % COMMAND_QUEUE_LENGTH;
this->state_ = STATE_IDLE;
} else {
// crc failed
// no log message necessary, check_incoming_crc_() logs
this->command_queue_[this->command_queue_position_].clear();
this->command_queue_[this->command_queue_position_] = std::string("");
this->command_queue_position_ = (command_queue_position_ + 1) % COMMAND_QUEUE_LENGTH;
this->state_ = STATE_IDLE;
}
} else {
ESP_LOGD(TAG, "command %s response length not OK: with length %zu",
this->command_queue_[this->command_queue_position_].c_str(), this->read_pos_);
this->command_queue_[this->command_queue_position_].clear();
this->command_queue_[this->command_queue_position_] = std::string("");
this->command_queue_position_ = (command_queue_position_ + 1) % COMMAND_QUEUE_LENGTH;
this->state_ = STATE_IDLE;
}
@@ -127,7 +127,7 @@ void Pipsolar::loop() {
const char *command = this->command_queue_[this->command_queue_position_].c_str();
this->command_start_millis_ = millis();
ESP_LOGD(TAG, "command %s timeout", command);
this->command_queue_[this->command_queue_position_].clear();
this->command_queue_[this->command_queue_position_] = std::string("");
this->command_queue_position_ = (command_queue_position_ + 1) % COMMAND_QUEUE_LENGTH;
this->state_ = STATE_IDLE;
return;
@@ -722,7 +722,7 @@ void Pipsolar::publish_binary_sensor_(esphome::optional<bool> b, binary_sensor::
}
}
esphome::optional<bool> Pipsolar::get_bit_(const std::string &bits, uint8_t bit_pos) {
esphome::optional<bool> Pipsolar::get_bit_(std::string bits, uint8_t bit_pos) {
if (bit_pos >= bits.length()) {
return {};
}

View File

@@ -224,7 +224,7 @@ class Pipsolar : public uart::UARTDevice, public PollingComponent {
void publish_binary_sensor_(esphome::optional<bool> b, binary_sensor::BinarySensor *sensor);
esphome::optional<bool> get_bit_(const std::string &bits, uint8_t bit_pos);
esphome::optional<bool> get_bit_(std::string bits, uint8_t bit_pos);
std::string command_queue_[COMMAND_QUEUE_LENGTH];
uint8_t command_queue_position_ = 0;

View File

@@ -80,21 +80,23 @@ class Select : public EntityBase {
void add_on_state_callback(std::function<void(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.
*
* IMPORTANT: At least ONE of the two control() methods must be overridden by derived classes.

View File

@@ -13,14 +13,14 @@ static const uint16_t SHTCX_COMMAND_READ_ID_REGISTER = 0xEFC8;
static const uint16_t SHTCX_COMMAND_SOFT_RESET = 0x805D;
static const uint16_t SHTCX_COMMAND_POLLING_H = 0x7866;
inline const char *to_string(SHTCXType type) {
static const LogString *shtcx_type_to_string(SHTCXType type) {
switch (type) {
case SHTCX_TYPE_SHTC3:
return "SHTC3";
return LOG_STR("SHTC3");
case SHTCX_TYPE_SHTC1:
return "SHTC1";
return LOG_STR("SHTC1");
default:
return "UNKNOWN";
return LOG_STR("UNKNOWN");
}
}
@@ -52,7 +52,7 @@ void SHTCXComponent::dump_config() {
ESP_LOGCONFIG(TAG,
"SHTCx:\n"
" Model: %s (%04x)",
to_string(this->type_), this->sensor_id_);
LOG_STR_ARG(shtcx_type_to_string(this->type_)), this->sensor_id_);
LOG_I2C_DEVICE(this);
if (this->is_failed()) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);

View File

@@ -51,7 +51,7 @@ void Sim800LComponent::update() {
} else if (state_ == STATE_RECEIVED_SMS) {
// Serial Buffer should have flushed.
// Send cmd to delete received sms
char delete_cmd[20]; // "AT+CMGD=" (8) + int (max 11) + null = 20
char delete_cmd[20]; // "AT+CMGD=" (8) + uint8_t (max 3) + null = 12 <= 20
buf_append_printf(delete_cmd, sizeof(delete_cmd), 0, "AT+CMGD=%d", this->parse_index_);
this->send_cmd_(delete_cmd);
this->state_ = STATE_CHECK_SMS;

View File

@@ -106,7 +106,7 @@ std::string bytes_repr(const BytesView &buffer) {
for (auto const value : buffer) {
// max 3: 2 hex digits + null
char hex_buf[3];
snprintf(hex_buf, sizeof(hex_buf), "%02x", value & 0xff);
snprintf(hex_buf, sizeof(hex_buf), "%02x", static_cast<unsigned int>(value));
repr += hex_buf;
}
return repr;

View File

@@ -25,7 +25,7 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_SPEED): cv.invalid(
"Configuring individual speeds is deprecated."
),
cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1, max=255),
cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1),
cv.Optional(CONF_PRESET_MODES): validate_preset_modes,
}
)

View File

@@ -10,7 +10,7 @@ namespace speed {
class SpeedFan : public Component, public fan::Fan {
public:
SpeedFan(uint8_t speed_count) : speed_count_(speed_count) {}
SpeedFan(int speed_count) : speed_count_(speed_count) {}
void setup() override;
void dump_config() override;
void set_output(output::FloatOutput *output) { this->output_ = output; }
@@ -26,7 +26,7 @@ class SpeedFan : public Component, public fan::Fan {
output::FloatOutput *output_;
output::BinaryOutput *oscillating_{nullptr};
output::BinaryOutput *direction_{nullptr};
uint8_t speed_count_{};
int speed_count_{};
fan::FanTraits traits_;
std::vector<const char *> preset_modes_{};
};

View File

@@ -43,13 +43,11 @@ SprinklerControllerSwitch::SprinklerControllerSwitch()
: turn_on_trigger_(new Trigger<>()), turn_off_trigger_(new Trigger<>()) {}
void SprinklerControllerSwitch::loop() {
if (!this->f_.has_value())
return;
// Loop is only enabled when f_ has a value (see setup())
auto s = (*this->f_)();
if (!s.has_value())
return;
this->publish_state(*s);
if (s.has_value()) {
this->publish_state(*s);
}
}
void SprinklerControllerSwitch::write_state(bool state) {
@@ -74,7 +72,13 @@ float SprinklerControllerSwitch::get_setup_priority() const { return setup_prior
Trigger<> *SprinklerControllerSwitch::get_turn_on_trigger() const { return this->turn_on_trigger_; }
Trigger<> *SprinklerControllerSwitch::get_turn_off_trigger() const { return this->turn_off_trigger_; }
void SprinklerControllerSwitch::setup() { this->state = this->get_initial_state_with_restore_mode().value_or(false); }
void SprinklerControllerSwitch::setup() {
this->state = this->get_initial_state_with_restore_mode().value_or(false);
// Disable loop if no state lambda is set - nothing to poll
if (!this->f_.has_value()) {
this->disable_loop();
}
}
void SprinklerControllerSwitch::dump_config() { LOG_SWITCH("", "Sprinkler Switch", this); }
@@ -327,25 +331,32 @@ SprinklerValveOperator *SprinklerValveRunRequest::valve_operator() { return this
SprinklerValveRunRequestOrigin SprinklerValveRunRequest::request_is_from() { return this->origin_; }
Sprinkler::Sprinkler() {}
Sprinkler::Sprinkler(const std::string &name) {
// The `name` is needed to set timers up, hence non-default constructor
// replaces `set_name()` method previously existed
this->name_ = name;
Sprinkler::Sprinkler() : Sprinkler("") {}
Sprinkler::Sprinkler(const char *name) : name_(name) {
// The `name` is stored for dump_config logging
this->timer_.init(2);
this->timer_.push_back({this->name_ + "sm", false, 0, 0, std::bind(&Sprinkler::sm_timer_callback_, this)});
this->timer_.push_back({this->name_ + "vs", false, 0, 0, std::bind(&Sprinkler::valve_selection_callback_, this)});
// Timer names only need to be unique within this component instance
this->timer_.push_back({"sm", false, 0, 0, std::bind(&Sprinkler::sm_timer_callback_, this)});
this->timer_.push_back({"vs", false, 0, 0, std::bind(&Sprinkler::valve_selection_callback_, this)});
}
void Sprinkler::setup() { this->all_valves_off_(true); }
void Sprinkler::setup() {
this->all_valves_off_(true);
// Start with loop disabled - nothing to do when idle
this->disable_loop();
}
void Sprinkler::loop() {
for (auto &vo : this->valve_op_) {
vo.loop();
}
if (this->prev_req_.has_request() && this->prev_req_.has_valve_operator() &&
this->prev_req_.valve_operator()->state() == IDLE) {
this->prev_req_.reset();
if (this->prev_req_.has_request()) {
if (this->prev_req_.has_valve_operator() && this->prev_req_.valve_operator()->state() == IDLE) {
this->prev_req_.reset();
}
} else if (this->state_ == IDLE) {
// Nothing more to do - disable loop until next activation
this->disable_loop();
}
}
@@ -1333,6 +1344,8 @@ void Sprinkler::start_valve_(SprinklerValveRunRequest *req) {
if (!this->is_a_valid_valve(req->valve())) {
return; // we can't do anything if the valve number isn't valid
}
// Enable loop to monitor valve operator states
this->enable_loop();
for (auto &vo : this->valve_op_) { // find the first available SprinklerValveOperator, load it and start it up
if (vo.state() == IDLE) {
auto run_duration = req->run_duration() ? req->run_duration() : this->valve_run_duration_adjusted(req->valve());
@@ -1575,8 +1588,7 @@ const LogString *Sprinkler::state_as_str_(SprinklerState state) {
void Sprinkler::start_timer_(const SprinklerTimerIndex timer_index) {
if (this->timer_duration_(timer_index) > 0) {
// FixedVector ensures timer_ can't be resized, so .c_str() pointers remain valid
this->set_timeout(this->timer_[timer_index].name.c_str(), this->timer_duration_(timer_index),
this->set_timeout(this->timer_[timer_index].name, this->timer_duration_(timer_index),
this->timer_cbf_(timer_index));
this->timer_[timer_index].start_time = millis();
this->timer_[timer_index].active = true;
@@ -1587,7 +1599,7 @@ void Sprinkler::start_timer_(const SprinklerTimerIndex timer_index) {
bool Sprinkler::cancel_timer_(const SprinklerTimerIndex timer_index) {
this->timer_[timer_index].active = false;
return this->cancel_timeout(this->timer_[timer_index].name.c_str());
return this->cancel_timeout(this->timer_[timer_index].name);
}
bool Sprinkler::timer_active_(const SprinklerTimerIndex timer_index) { return this->timer_[timer_index].active; }
@@ -1618,7 +1630,7 @@ void Sprinkler::sm_timer_callback_() {
}
void Sprinkler::dump_config() {
ESP_LOGCONFIG(TAG, "Sprinkler Controller -- %s", this->name_.c_str());
ESP_LOGCONFIG(TAG, "Sprinkler Controller -- %s", this->name_);
if (this->manual_selection_delay_.has_value()) {
ESP_LOGCONFIG(TAG, " Manual Selection Delay: %" PRIu32 " seconds", this->manual_selection_delay_.value_or(0));
}

View File

@@ -11,7 +11,7 @@
namespace esphome::sprinkler {
const std::string MIN_STR = "min";
inline constexpr const char *MIN_STR = "min";
enum SprinklerState : uint8_t {
// NOTE: these states are used by both SprinklerValveOperator and Sprinkler (the controller)!
@@ -49,7 +49,7 @@ struct SprinklerQueueItem {
};
struct SprinklerTimer {
const std::string name;
const char *name;
bool active;
uint32_t time;
uint32_t start_time;
@@ -176,7 +176,7 @@ class SprinklerValveRunRequest {
class Sprinkler : public Component {
public:
Sprinkler();
Sprinkler(const std::string &name);
Sprinkler(const char *name);
void setup() override;
void loop() override;
void dump_config() override;
@@ -504,7 +504,7 @@ class Sprinkler : public Component {
uint32_t start_delay_{0};
uint32_t stop_delay_{0};
std::string name_;
const char *name_{""};
/// Sprinkler controller state
SprinklerState state_{IDLE};

View File

@@ -126,10 +126,9 @@ void StatsdComponent::update() {
}
out.append(s.name);
// Buffer for ":" + value + "|g\n".
// %g uses max 13 chars for value (sign + 6 significant digits + e+xxx)
// Total: 1 + 13 + 4 = 18 chars + null, use 24 for safety
char val_buf[24];
buf_append_printf(val_buf, sizeof(val_buf), 0, ":%g|g\n", val);
// %f with -DBL_MAX can produce up to 321 chars, plus ":" and "|g\n" (4) + null = 326
char val_buf[330];
buf_append_printf(val_buf, sizeof(val_buf), 0, ":%f|g\n", val);
out.append(val_buf);
if (out.length() > SEND_THRESHOLD) {

View File

@@ -7,14 +7,14 @@ DEPENDENCIES = ["network"]
status_ns = cg.esphome_ns.namespace("status")
StatusBinarySensor = status_ns.class_(
"StatusBinarySensor", binary_sensor.BinarySensor, cg.Component
"StatusBinarySensor", binary_sensor.BinarySensor, cg.PollingComponent
)
CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(
StatusBinarySensor,
device_class=DEVICE_CLASS_CONNECTIVITY,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
).extend(cv.COMPONENT_SCHEMA)
).extend(cv.polling_component_schema("1s"))
async def to_code(config):

View File

@@ -10,12 +10,11 @@
#include "esphome/components/api/api_server.h"
#endif
namespace esphome {
namespace status {
namespace esphome::status {
static const char *const TAG = "status";
void StatusBinarySensor::loop() {
void StatusBinarySensor::update() {
bool status = network::is_connected();
#ifdef USE_MQTT
if (mqtt::global_mqtt_client != nullptr) {
@@ -33,5 +32,4 @@ void StatusBinarySensor::loop() {
void StatusBinarySensor::setup() { this->publish_initial_state(false); }
void StatusBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "Status Binary Sensor", this); }
} // namespace status
} // namespace esphome
} // namespace esphome::status

View File

@@ -3,12 +3,11 @@
#include "esphome/core/component.h"
#include "esphome/components/binary_sensor/binary_sensor.h"
namespace esphome {
namespace status {
namespace esphome::status {
class StatusBinarySensor : public binary_sensor::BinarySensor, public Component {
class StatusBinarySensor : public binary_sensor::BinarySensor, public PollingComponent {
public:
void loop() override;
void update() override;
void setup() override;
void dump_config() override;
@@ -16,5 +15,4 @@ class StatusBinarySensor : public binary_sensor::BinarySensor, public Component
bool is_status_binary_sensor() const override { return true; }
};
} // namespace status
} // namespace esphome
} // namespace esphome::status

View File

@@ -4,7 +4,8 @@ namespace esphome {
namespace teleinfo {
static const char *const TAG = "teleinfo_sensor";
void TeleInfoSensor::publish_val(const char *val) {
TeleInfoSensor::TeleInfoSensor(const char *tag) { this->tag = std::string(tag); }
void TeleInfoSensor::publish_val(const std::string &val) {
auto newval = parse_number<float>(val).value_or(0.0f);
publish_state(newval);
}

View File

@@ -7,8 +7,8 @@ namespace teleinfo {
class TeleInfoSensor : public TeleInfoListener, public sensor::Sensor, public Component {
public:
TeleInfoSensor(const char *tag) { this->tag = tag; }
void publish_val(const char *val) override;
TeleInfoSensor(const char *tag);
void publish_val(const std::string &val) override;
void dump_config() override;
};

View File

@@ -172,15 +172,15 @@ void TeleInfo::loop() {
/* Advance buf_finger to end of group */
buf_finger += field_len + 1 + 1 + 1;
publish_value_(tag_, val_);
publish_value_(std::string(tag_), std::string(val_));
}
state_ = OFF;
break;
}
}
void TeleInfo::publish_value_(const char *tag, const char *val) {
void TeleInfo::publish_value_(const std::string &tag, const std::string &val) {
for (auto *element : teleinfo_listeners_) {
if (strcmp(tag, element->tag) != 0)
if (tag != element->tag)
continue;
element->publish_val(val);
}

View File

@@ -18,8 +18,8 @@ static const uint16_t MAX_TIMESTAMP_SIZE = 14;
class TeleInfoListener {
public:
const char *tag{nullptr};
virtual void publish_val(const char *val){};
std::string tag;
virtual void publish_val(const std::string &val){};
};
class TeleInfo : public PollingComponent, public uart::UARTDevice {
public:
@@ -48,7 +48,7 @@ class TeleInfo : public PollingComponent, public uart::UARTDevice {
} state_{OFF};
bool read_chars_until_(bool drop, uint8_t c);
bool check_crc_(const char *grp, const char *grp_end);
void publish_value_(const char *tag, const char *val);
void publish_value_(const std::string &tag, const std::string &val);
};
} // namespace teleinfo
} // namespace esphome

View File

@@ -4,7 +4,8 @@ namespace esphome {
namespace teleinfo {
static const char *const TAG = "teleinfo_text_sensor";
void TeleInfoTextSensor::publish_val(const char *val) { publish_state(val); }
TeleInfoTextSensor::TeleInfoTextSensor(const char *tag) { this->tag = std::string(tag); }
void TeleInfoTextSensor::publish_val(const std::string &val) { publish_state(val); }
void TeleInfoTextSensor::dump_config() { LOG_TEXT_SENSOR(" ", "Teleinfo Text Sensor", this); }
} // namespace teleinfo
} // namespace esphome

View File

@@ -5,8 +5,8 @@ namespace esphome {
namespace teleinfo {
class TeleInfoTextSensor : public TeleInfoListener, public text_sensor::TextSensor, public Component {
public:
TeleInfoTextSensor(const char *tag) { this->tag = tag; }
void publish_val(const char *val) override;
TeleInfoTextSensor(const char *tag);
void publish_val(const std::string &val) override;
void dump_config() override;
};
} // namespace teleinfo

View File

@@ -19,7 +19,7 @@ CONFIG_SCHEMA = (
{
cv.Optional(CONF_HAS_DIRECTION, default=False): cv.boolean,
cv.Optional(CONF_HAS_OSCILLATING, default=False): cv.boolean,
cv.Optional(CONF_SPEED_COUNT): cv.int_range(min=1, max=255),
cv.Optional(CONF_SPEED_COUNT): cv.int_range(min=1),
cv.Optional(CONF_PRESET_MODES): validate_preset_modes,
}
)

View File

@@ -12,7 +12,7 @@ class TemplateFan final : public Component, public fan::Fan {
void dump_config() override;
void set_has_direction(bool has_direction) { this->has_direction_ = has_direction; }
void set_has_oscillating(bool has_oscillating) { this->has_oscillating_ = has_oscillating; }
void set_speed_count(uint8_t count) { this->speed_count_ = count; }
void set_speed_count(int count) { this->speed_count_ = count; }
void set_preset_modes(std::initializer_list<const char *> presets) { this->preset_modes_ = presets; }
fan::FanTraits get_traits() override { return this->traits_; }
@@ -21,7 +21,7 @@ class TemplateFan final : public Component, public fan::Fan {
bool has_oscillating_{false};
bool has_direction_{false};
uint8_t speed_count_{0};
int speed_count_{0};
fan::FanTraits traits_;
std::vector<const char *> preset_modes_{};
};

View File

@@ -22,7 +22,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_SPEED_DATAPOINT): cv.uint8_t,
cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t,
cv.Optional(CONF_DIRECTION_DATAPOINT): cv.uint8_t,
cv.Optional(CONF_SPEED_COUNT, default=3): cv.int_range(min=1, max=255),
cv.Optional(CONF_SPEED_COUNT, default=3): cv.int_range(min=1, max=256),
}
)
.extend(cv.COMPONENT_SCHEMA),

View File

@@ -9,7 +9,7 @@ namespace tuya {
class TuyaFan : public Component, public fan::Fan {
public:
TuyaFan(Tuya *parent, uint8_t speed_count) : parent_(parent), speed_count_(speed_count) {}
TuyaFan(Tuya *parent, int speed_count) : parent_(parent), speed_count_(speed_count) {}
void setup() override;
void dump_config() override;
void set_speed_id(uint8_t speed_id) { this->speed_id_ = speed_id; }
@@ -27,7 +27,7 @@ class TuyaFan : public Component, public fan::Fan {
optional<uint8_t> switch_id_{};
optional<uint8_t> oscillation_id_{};
optional<uint8_t> direction_id_{};
uint8_t speed_count_{};
int speed_count_{};
TuyaDatapointType speed_type_{};
TuyaDatapointType oscillation_type_{};
};

View File

@@ -24,7 +24,7 @@ void TuyaTextSensor::setup() {
}
case TuyaDatapointType::ENUM: {
char buf[4]; // uint8_t max is 3 digits + null
snprintf(buf, sizeof(buf), "%u", datapoint.value_enum);
buf_append_printf(buf, sizeof(buf), 0, "%u", datapoint.value_enum);
ESP_LOGD(TAG, "MCU reported text sensor %u is: %s", datapoint.id, buf);
this->publish_state(buf);
break;

View File

@@ -2,7 +2,7 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <array>
#include <vector>
namespace esphome {
namespace tx20 {
@@ -45,24 +45,25 @@ std::string Tx20Component::get_wind_cardinal_direction() const { return this->wi
void Tx20Component::decode_and_publish_() {
ESP_LOGVV(TAG, "Decode Tx20");
std::array<bool, MAX_BUFFER_SIZE> bit_buffer{};
size_t bit_pos = 0;
std::string string_buffer;
std::string string_buffer_2;
std::vector<bool> bit_buffer;
bool current_bit = true;
// Cap at MAX_BUFFER_SIZE to prevent out-of-bounds access (buffer_index can exceed MAX_BUFFER_SIZE in ISR)
const int max_buffer_index = std::min(static_cast<int>(this->store_.buffer_index), static_cast<int>(MAX_BUFFER_SIZE));
for (int i = 1; i <= max_buffer_index; i++) {
for (int i = 1; i <= this->store_.buffer_index; i++) {
string_buffer_2 += to_string(this->store_.buffer[i]) + ", ";
uint8_t repeat = this->store_.buffer[i] / TX20_BIT_TIME;
// ignore segments at the end that were too short
for (uint8_t j = 0; j < repeat && bit_pos < MAX_BUFFER_SIZE; j++) {
bit_buffer[bit_pos++] = current_bit;
}
string_buffer.append(repeat, current_bit ? '1' : '0');
bit_buffer.insert(bit_buffer.end(), repeat, current_bit);
current_bit = !current_bit;
}
current_bit = !current_bit;
size_t bits_before_padding = bit_pos;
while (bit_pos < MAX_BUFFER_SIZE) {
bit_buffer[bit_pos++] = current_bit;
if (string_buffer.length() < MAX_BUFFER_SIZE) {
uint8_t remain = MAX_BUFFER_SIZE - string_buffer.length();
string_buffer_2 += to_string(remain) + ", ";
string_buffer.append(remain, current_bit ? '1' : '0');
bit_buffer.insert(bit_buffer.end(), remain, current_bit);
}
uint8_t tx20_sa = 0;
@@ -107,24 +108,8 @@ void Tx20Component::decode_and_publish_() {
// 2. Check received checksum matches calculated checksum
// 3. Check that Wind Direction matches Wind Direction (Inverted)
// 4. Check that Wind Speed matches Wind Speed (Inverted)
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
// Build debug strings from completed data
char debug_buf[320]; // buffer values: max 42 entries * 7 chars each
size_t debug_pos = 0;
for (int i = 1; i <= max_buffer_index; i++) {
debug_pos = buf_append_printf(debug_buf, sizeof(debug_buf), debug_pos, "%u, ", this->store_.buffer[i]);
}
if (bits_before_padding < MAX_BUFFER_SIZE) {
buf_append_printf(debug_buf, sizeof(debug_buf), debug_pos, "%zu, ", MAX_BUFFER_SIZE - bits_before_padding);
}
char bits_buf[MAX_BUFFER_SIZE + 1];
for (size_t i = 0; i < MAX_BUFFER_SIZE; i++) {
bits_buf[i] = bit_buffer[i] ? '1' : '0';
}
bits_buf[MAX_BUFFER_SIZE] = '\0';
ESP_LOGVV(TAG, "BUFFER %s", debug_buf);
ESP_LOGVV(TAG, "Decoded bits %s", bits_buf);
#endif
ESP_LOGVV(TAG, "BUFFER %s", string_buffer_2.c_str());
ESP_LOGVV(TAG, "Decoded bits %s", string_buffer.c_str());
if (tx20_sa == 4) {
if (chk == tx20_sd) {

View File

@@ -81,6 +81,8 @@ struct Timer {
this->id.c_str(), this->name.c_str(), this->total_seconds, this->seconds_left, YESNO(this->is_active));
return buffer.data();
}
// Remove before 2026.8.0
ESPDEPRECATED("Use to_str() instead. Removed in 2026.8.0", "2026.2.0")
std::string to_string() const {
char buffer[TO_STR_BUFFER_SIZE];
return this->to_str(buffer);

View File

@@ -406,9 +406,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

@@ -1735,7 +1735,6 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() {
(old_priority > std::numeric_limits<int8_t>::min()) ? (old_priority - 1) : std::numeric_limits<int8_t>::min();
this->set_sta_priority(failed_bssid.value(), new_priority);
}
char bssid_s[18];
format_mac_addr_upper(failed_bssid.value().data(), bssid_s);
ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d",

View File

@@ -79,13 +79,17 @@ async def setup_conf(config, key):
async def to_code(config):
# Request specific WiFi listeners based on which sensors are configured
# Each sensor needs its own listener slot - call request for EACH sensor
# SSID and BSSID use WiFiConnectStateListener
if CONF_SSID in config or CONF_BSSID in config:
wifi.request_wifi_connect_state_listener()
for key in (CONF_SSID, CONF_BSSID):
if key in config:
wifi.request_wifi_connect_state_listener()
# IP address and DNS use WiFiIPStateListener
if CONF_IP_ADDRESS in config or CONF_DNS_ADDRESS in config:
wifi.request_wifi_ip_state_listener()
for key in (CONF_IP_ADDRESS, CONF_DNS_ADDRESS):
if key in config:
wifi.request_wifi_ip_state_listener()
# Scan results use WiFiScanResultsListener
if CONF_SCAN_RESULTS in config:

View File

@@ -6,7 +6,7 @@ namespace x9c {
static const char *const TAG = "x9c.output";
void X9cOutput::trim_value(int change_amount) {
void X9cOutput::trim_value(int32_t change_amount) {
if (change_amount == 0) {
return;
}
@@ -47,17 +47,17 @@ void X9cOutput::setup() {
if (this->initial_value_ <= 0.50) {
this->trim_value(-101); // Set min value (beyond 0)
this->trim_value(static_cast<uint32_t>(roundf(this->initial_value_ * 100)));
this->trim_value(lroundf(this->initial_value_ * 100));
} else {
this->trim_value(101); // Set max value (beyond 100)
this->trim_value(static_cast<uint32_t>(roundf(this->initial_value_ * 100) - 100));
this->trim_value(lroundf(this->initial_value_ * 100) - 100);
}
this->pot_value_ = this->initial_value_;
this->write_state(this->initial_value_);
}
void X9cOutput::write_state(float state) {
this->trim_value(static_cast<uint32_t>(roundf((state - this->pot_value_) * 100)));
this->trim_value(lroundf((state - this->pot_value_) * 100));
this->pot_value_ = state;
}

View File

@@ -18,7 +18,7 @@ class X9cOutput : public output::FloatOutput, public Component {
void setup() override;
void dump_config() override;
void trim_value(int change_amount);
void trim_value(int32_t change_amount);
protected:
void write_state(float state) override;

View File

@@ -1046,20 +1046,20 @@ def mac_address(value):
return core.MACAddress(*parts_int)
def bind_key(value):
def bind_key(value, *, name="Bind key"):
value = string_strict(value)
parts = [value[i : i + 2] for i in range(0, len(value), 2)]
if len(parts) != 16:
raise Invalid("Bind key must consist of 16 hexadecimal numbers")
raise Invalid(f"{name} must consist of 16 hexadecimal numbers")
parts_int = []
if any(len(part) != 2 for part in parts):
raise Invalid("Bind key must be format XX")
raise Invalid(f"{name} must be format XX")
for part in parts:
try:
parts_int.append(int(part, 16))
except ValueError:
# pylint: disable=raise-missing-from
raise Invalid("Bind key must be hex values from 00 to FF")
raise Invalid(f"{name} must be hex values from 00 to FF")
return "".join(f"{part:02X}" for part in parts_int)

View File

@@ -761,15 +761,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

@@ -347,10 +347,7 @@ std::string format_hex(const uint8_t *data, size_t length) {
format_hex_to(&ret[0], length * 2 + 1, data, length);
return ret;
}
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
std::string format_hex(const std::vector<uint8_t> &data) { return format_hex(data.data(), data.size()); }
#pragma GCC diagnostic pop
char *format_hex_pretty_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator) {
return format_hex_internal(buffer, buffer_size, data, length, separator, 'A');
@@ -519,8 +516,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/base64url character in the lookup table.
// Returns the character's position (0-63) if found, or 0 if not found.

View File

@@ -132,78 +132,6 @@ template<typename T> class ConstVector {
size_t size_;
};
/// Small buffer optimization - stores data inline when small, heap-allocates for large data
/// This avoids heap fragmentation for common small allocations while supporting arbitrary sizes.
/// Memory management is encapsulated - callers just use set() and data().
template<size_t InlineSize = 8> class SmallInlineBuffer {
public:
SmallInlineBuffer() = default;
~SmallInlineBuffer() {
if (!this->is_inline_())
delete[] this->heap_;
}
// Move constructor
SmallInlineBuffer(SmallInlineBuffer &&other) noexcept : len_(other.len_) {
if (other.is_inline_()) {
memcpy(this->inline_, other.inline_, this->len_);
} else {
this->heap_ = other.heap_;
other.heap_ = nullptr;
}
other.len_ = 0;
}
// Move assignment
SmallInlineBuffer &operator=(SmallInlineBuffer &&other) noexcept {
if (this != &other) {
if (!this->is_inline_())
delete[] this->heap_;
this->len_ = other.len_;
if (other.is_inline_()) {
memcpy(this->inline_, other.inline_, this->len_);
} else {
this->heap_ = other.heap_;
other.heap_ = nullptr;
}
other.len_ = 0;
}
return *this;
}
// Disable copy (would need deep copy of heap data)
SmallInlineBuffer(const SmallInlineBuffer &) = delete;
SmallInlineBuffer &operator=(const SmallInlineBuffer &) = delete;
/// Set buffer contents, allocating heap if needed
void set(const uint8_t *src, size_t size) {
// Free existing heap allocation if switching from heap to inline or different heap size
if (!this->is_inline_() && (size <= InlineSize || size != this->len_)) {
delete[] this->heap_;
this->heap_ = nullptr; // Defensive: prevent use-after-free if logic changes
}
// Allocate new heap buffer if needed
if (size > InlineSize && (this->is_inline_() || size != this->len_)) {
this->heap_ = new uint8_t[size]; // NOLINT(cppcoreguidelines-owning-memory)
}
this->len_ = size;
memcpy(this->data(), src, size);
}
uint8_t *data() { return this->is_inline_() ? this->inline_ : this->heap_; }
const uint8_t *data() const { return this->is_inline_() ? this->inline_ : this->heap_; }
size_t size() const { return this->len_; }
protected:
bool is_inline_() const { return this->len_ <= InlineSize; }
size_t len_{0};
union {
uint8_t inline_[InlineSize]{}; // Zero-init ensures clean initial state
uint8_t *heap_;
};
};
/// Minimal static vector - saves memory by avoiding std::vector overhead
template<typename T, size_t N> class StaticVector {
public:
@@ -238,9 +166,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_; }
// Direct access to underlying data
T *data() { return data_.data(); }
const T *data() const { return data_.data(); }
@@ -423,6 +348,8 @@ template<typename T> class FixedVector {
size_t size() const { return size_; }
bool empty() const { return size_ == 0; }
size_t capacity() const { return capacity_; }
bool full() const { return size_ == capacity_; }
/// Access element without bounds checking (matches std::vector behavior)
/// Caller must ensure index is valid (i < size())
@@ -441,6 +368,35 @@ template<typename T> class FixedVector {
const T *end() const { return data_ + size_; }
};
/// @brief Helper class for efficient buffer allocation - uses stack for small sizes, heap for large
/// This is useful when most operations need a small buffer but occasionally need larger ones.
/// The stack buffer avoids heap allocation in the common case, while heap fallback handles edge cases.
template<size_t STACK_SIZE> class SmallBufferWithHeapFallback {
public:
explicit SmallBufferWithHeapFallback(size_t size) {
if (size <= STACK_SIZE) {
this->buffer_ = this->stack_buffer_;
} else {
this->heap_buffer_ = new uint8_t[size];
this->buffer_ = this->heap_buffer_;
}
}
~SmallBufferWithHeapFallback() { delete[] this->heap_buffer_; }
// Delete copy and move operations to prevent double-delete
SmallBufferWithHeapFallback(const SmallBufferWithHeapFallback &) = delete;
SmallBufferWithHeapFallback &operator=(const SmallBufferWithHeapFallback &) = delete;
SmallBufferWithHeapFallback(SmallBufferWithHeapFallback &&) = delete;
SmallBufferWithHeapFallback &operator=(SmallBufferWithHeapFallback &&) = delete;
uint8_t *get() { return this->buffer_; }
private:
uint8_t stack_buffer_[STACK_SIZE];
uint8_t *heap_buffer_{nullptr};
uint8_t *buffer_;
};
///@}
/// @name Mathematics
@@ -614,10 +570,6 @@ template<typename T> constexpr T convert_little_endian(T val) {
bool str_equals_case_insensitive(const std::string &a, const std::string &b);
/// Compare StringRefs for equality in case-insensitive manner.
bool str_equals_case_insensitive(StringRef a, StringRef b);
/// Compare C strings for equality in case-insensitive manner (no heap allocation).
inline bool str_equals_case_insensitive(const char *a, const char *b) { return strcasecmp(a, b) == 0; }
inline bool str_equals_case_insensitive(const std::string &a, const char *b) { return strcasecmp(a.c_str(), b) == 0; }
inline bool str_equals_case_insensitive(const char *a, const std::string &b) { return strcasecmp(a, b.c_str()) == 0; }
/// Check whether a string starts with a value.
bool str_startswith(const std::string &str, const std::string &start);
@@ -695,11 +647,9 @@ inline uint32_t fnv1_hash_object_id(const char *str, size_t len) {
}
/// snprintf-like function returning std::string of maximum length \p len (excluding null terminator).
/// @warning Allocates heap memory. Use snprintf() with a stack buffer instead.
std::string __attribute__((format(printf, 1, 3))) str_snprintf(const char *fmt, size_t len, ...);
/// sprintf-like function returning std::string.
/// @warning Allocates heap memory. Use snprintf() with a stack buffer instead.
std::string __attribute__((format(printf, 1, 2))) str_sprintf(const char *fmt, ...);
#ifdef USE_ESP8266
@@ -1072,17 +1022,13 @@ std::string format_hex(const std::vector<uint8_t> &data);
/// Causes heap fragmentation on long-running devices.
template<typename T, enable_if_t<std::is_unsigned<T>::value, int> = 0> std::string format_hex(T val) {
val = convert_big_endian(val);
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
return format_hex(reinterpret_cast<uint8_t *>(&val), sizeof(T));
#pragma GCC diagnostic pop
}
/// Format the std::array \p data in lowercased hex.
/// @warning Allocates heap memory. Use format_hex_to() with a stack buffer instead.
/// Causes heap fragmentation on long-running devices.
template<std::size_t N> std::string format_hex(const std::array<uint8_t, N> &data) {
return format_hex(data.data(), data.size());
#pragma GCC diagnostic pop
}
/** Format a byte array in pretty-printed, human-readable hex format.

View File

@@ -118,9 +118,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)

Some files were not shown because too many files have changed in this diff Show More