Compare commits

..

85 Commits

Author SHA1 Message Date
J. Nick Koston
0b60fd0c8c [core] Avoid heap allocation in str_equals_case_insensitive with string literals (#13312) 2026-01-20 21:49:14 -10:00
J. Nick Koston
fc16ad806a [ci] Block sprintf/vsprintf usage, suggest snprintf alternatives (#13305) 2026-01-20 17:53:36 -10:00
J. Nick Koston
7e43abd86f [web_server_idf] Use direct member for ListEntitiesIterator instead of unique_ptr (#13405) 2026-01-20 17:53:23 -10:00
J. Nick Koston
7a2734fae9 [libretiny] Disable unused LWIP statistics to save RAM and flash (#13404) 2026-01-20 17:53:10 -10:00
J. Nick Koston
346f3d38d5 [logger] Use raw pointer for task log buffer to match tx_buffer pattern (#13402) 2026-01-20 17:52:58 -10:00
J. Nick Koston
fbde91358c [mdns] Use stack buffer for txt records on ESP32 (#13401) 2026-01-20 17:52:43 -10:00
J. Nick Koston
54d6825323 [esp32] [libretiny] Use stack buffer for preference comparison (#13398) 2026-01-20 17:52:28 -10:00
J. Nick Koston
307c3e1061 [core] Simplify LazyCallbackManager memory management (#13387) 2026-01-20 17:52:12 -10:00
Jonathan Swoboda
df74d307c8 [esp32] Add support for native ESP-IDF builds (#13272)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-01-20 22:52:04 -05:00
Jonathan Swoboda
acdc7bd892 [json] Use ESP-IDF component registry for ArduinoJson on ESP32 (#13280)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 22:51:54 -05:00
Jasper van der Neut - Stulen
1095bde2db [cc1101] Add on_packet listener callback code (packet_transport) (#13344) 2026-01-20 22:51:39 -05:00
J. Nick Koston
258b73d7f6 [core] Eliminate global constructor overhead for component vectors (#13386) 2026-01-20 17:51:06 -10:00
J. Nick Koston
31608543c2 [esp32_ble_tracker] Optimize loop with state change tracking for ~85% CPU reduction (#13337) 2026-01-20 17:50:53 -10:00
J. Nick Koston
41a060668c [api] Use stack buffers for noise handshake messages (#13399)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-20 17:50:39 -10:00
J. Nick Koston
6bad697fc6 [debug] ESP8266: Eliminate heap allocations from Arduino String functions (#13352) 2026-01-20 17:50:27 -10:00
J. Nick Koston
3ca5e5e4e4 [wifi] ESP8266: Use direct SDK calls to reduce flash and heap allocation (#13349) 2026-01-20 17:50:13 -10:00
J. Nick Koston
cd4cb8b3ec [datetime] Add const char * overloads for string parsing to avoid heap allocation (#13363) 2026-01-20 17:50:01 -10:00
J. Nick Koston
1f3a0490a7 [wifi] Process scan results one at a time to avoid heap allocation (#13400) 2026-01-20 17:49:40 -10:00
Jonathan Swoboda
b08d871add Merge branch 'release' into dev 2026-01-20 22:43:22 -05:00
Jonathan Swoboda
15f0986a59 Merge pull request #13406 from esphome/bump-2026.1.0
2026.1.0
2026-01-20 22:43:06 -05:00
Jonathan Swoboda
90edf32acf Bump version to 2026.1.0 2026-01-20 21:15:02 -05:00
polyfloyd
3c0f43db9e Add the max_delta filter (#12605)
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2026-01-21 10:58:47 +11: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
164 changed files with 2511 additions and 864 deletions

View File

@@ -1 +1 @@
d272a88e8ca28ae9340a9a03295a566432a52cb696501908f57764475bf7ca65
d15ae81646ac0ee76b2586716fe697f187281523ee6db566aed26542a9f98d1a

View File

@@ -1,5 +1,6 @@
# PYTHON_ARGCOMPLETE_OK
import argparse
from collections.abc import Callable
from datetime import datetime
import functools
import getpass
@@ -42,6 +43,7 @@ from esphome.const import (
CONF_SUBSTITUTIONS,
CONF_TOPIC,
ENV_NOGITIGNORE,
KEY_NATIVE_IDF,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_RP2040,
@@ -115,6 +117,7 @@ class ArgsProtocol(Protocol):
configuration: str
name: str
upload_speed: str | None
native_idf: bool
def choose_prompt(options, purpose: str = None):
@@ -499,12 +502,15 @@ def wrap_to_code(name, comp):
return wrapped
def write_cpp(config: ConfigType) -> int:
def write_cpp(config: ConfigType, native_idf: bool = False) -> int:
if not get_bool_env(ENV_NOGITIGNORE):
writer.write_gitignore()
# Store native_idf flag so esp32 component can check it
CORE.data[KEY_NATIVE_IDF] = native_idf
generate_cpp_contents(config)
return write_cpp_file()
return write_cpp_file(native_idf=native_idf)
def generate_cpp_contents(config: ConfigType) -> None:
@@ -518,32 +524,54 @@ def generate_cpp_contents(config: ConfigType) -> None:
CORE.flush_tasks()
def write_cpp_file() -> int:
def write_cpp_file(native_idf: bool = False) -> int:
code_s = indent(CORE.cpp_main_section)
writer.write_cpp(code_s)
from esphome.build_gen import platformio
if native_idf and CORE.is_esp32 and CORE.target_framework == "esp-idf":
from esphome.build_gen import espidf
platformio.write_project()
espidf.write_project()
else:
from esphome.build_gen import platformio
platformio.write_project()
return 0
def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
from esphome import platformio_api
native_idf = getattr(args, "native_idf", False)
# NOTE: "Build path:" format is parsed by script/ci_memory_impact_extract.py
# If you change this format, update the regex in that script as well
_LOGGER.info("Compiling app... Build path: %s", CORE.build_path)
rc = platformio_api.run_compile(config, CORE.verbose)
if rc != 0:
return rc
if native_idf and CORE.is_esp32 and CORE.target_framework == "esp-idf":
from esphome import espidf_api
rc = espidf_api.run_compile(config, CORE.verbose)
if rc != 0:
return rc
# Create factory.bin and ota.bin
espidf_api.create_factory_bin()
espidf_api.create_ota_bin()
else:
from esphome import platformio_api
rc = platformio_api.run_compile(config, CORE.verbose)
if rc != 0:
return rc
idedata = platformio_api.get_idedata(config)
if idedata is None:
return 1
# Check if firmware was rebuilt and emit build_info + create manifest
_check_and_emit_build_info()
idedata = platformio_api.get_idedata(config)
return 0 if idedata is not None else 1
return 0
def _check_and_emit_build_info() -> None:
@@ -800,7 +828,8 @@ def command_vscode(args: ArgsProtocol) -> int | None:
def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None:
exit_code = write_cpp(config)
native_idf = getattr(args, "native_idf", False)
exit_code = write_cpp(config, native_idf=native_idf)
if exit_code != 0:
return exit_code
if args.only_generate:
@@ -855,7 +884,8 @@ def command_logs(args: ArgsProtocol, config: ConfigType) -> int | None:
def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
exit_code = write_cpp(config)
native_idf = getattr(args, "native_idf", False)
exit_code = write_cpp(config, native_idf=native_idf)
if exit_code != 0:
return exit_code
exit_code = compile_program(args, config)
@@ -936,11 +966,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):
@@ -950,17 +990,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
@@ -975,6 +1017,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:
@@ -983,6 +1027,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
@@ -1284,6 +1339,11 @@ def parse_args(argv):
help="Only generate source code, do not compile.",
action="store_true",
)
parser_compile.add_argument(
"--native-idf",
help="Build with native ESP-IDF instead of PlatformIO (ESP32 esp-idf framework only).",
action="store_true",
)
parser_upload = subparsers.add_parser(
"upload",
@@ -1365,6 +1425,11 @@ def parse_args(argv):
help="Reset the device before starting serial logs.",
default=os.getenv("ESPHOME_SERIAL_LOGGING_RESET"),
)
parser_run.add_argument(
"--native-idf",
help="Build with native ESP-IDF instead of PlatformIO (ESP32 esp-idf framework only).",
action="store_true",
)
parser_clean = subparsers.add_parser(
"clean-mqtt",
@@ -1533,38 +1598,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():

139
esphome/build_gen/espidf.py Normal file
View File

@@ -0,0 +1,139 @@
"""ESP-IDF direct build generator for ESPHome."""
import json
from pathlib import Path
from esphome.components.esp32 import get_esp32_variant
from esphome.core import CORE
from esphome.helpers import mkdir_p, write_file_if_changed
def get_available_components() -> list[str] | None:
"""Get list of available ESP-IDF components from project_description.json.
Returns only internal ESP-IDF components, excluding external/managed
components (from idf_component.yml).
"""
project_desc = Path(CORE.build_path) / "build" / "project_description.json"
if not project_desc.exists():
return None
try:
with open(project_desc, encoding="utf-8") as f:
data = json.load(f)
component_info = data.get("build_component_info", {})
result = []
for name, info in component_info.items():
# Exclude our own src component
if name == "src":
continue
# Exclude managed/external components
comp_dir = info.get("dir", "")
if "managed_components" in comp_dir:
continue
result.append(name)
return result
except (json.JSONDecodeError, OSError):
return None
def has_discovered_components() -> bool:
"""Check if we have discovered components from a previous configure."""
return get_available_components() is not None
def get_project_cmakelists() -> str:
"""Generate the top-level CMakeLists.txt for ESP-IDF project."""
# Get IDF target from ESP32 variant (e.g., ESP32S3 -> esp32s3)
variant = get_esp32_variant()
idf_target = variant.lower().replace("-", "")
return f"""\
# Auto-generated by ESPHome
cmake_minimum_required(VERSION 3.16)
set(IDF_TARGET {idf_target})
set(EXTRA_COMPONENT_DIRS ${{CMAKE_SOURCE_DIR}}/src)
include($ENV{{IDF_PATH}}/tools/cmake/project.cmake)
project({CORE.name})
"""
def get_component_cmakelists(minimal: bool = False) -> str:
"""Generate the main component CMakeLists.txt."""
idf_requires = [] if minimal else (get_available_components() or [])
requires_str = " ".join(idf_requires)
# Extract compile definitions from build flags (-DXXX -> XXX)
compile_defs = [flag[2:] for flag in CORE.build_flags if flag.startswith("-D")]
compile_defs_str = "\n ".join(compile_defs) if compile_defs else ""
# Extract compile options (-W flags, excluding linker flags)
compile_opts = [
flag
for flag in CORE.build_flags
if flag.startswith("-W") and not flag.startswith("-Wl,")
]
compile_opts_str = "\n ".join(compile_opts) if compile_opts else ""
# Extract linker options (-Wl, flags)
link_opts = [flag for flag in CORE.build_flags if flag.startswith("-Wl,")]
link_opts_str = "\n ".join(link_opts) if link_opts else ""
return f"""\
# Auto-generated by ESPHome
file(GLOB_RECURSE app_sources
"${{CMAKE_CURRENT_SOURCE_DIR}}/*.cpp"
"${{CMAKE_CURRENT_SOURCE_DIR}}/*.c"
"${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.cpp"
"${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.c"
)
idf_component_register(
SRCS ${{app_sources}}
INCLUDE_DIRS "." "esphome"
REQUIRES {requires_str}
)
# Apply C++ standard
target_compile_features(${{COMPONENT_LIB}} PUBLIC cxx_std_20)
# ESPHome compile definitions
target_compile_definitions(${{COMPONENT_LIB}} PUBLIC
{compile_defs_str}
)
# ESPHome compile options
target_compile_options(${{COMPONENT_LIB}} PUBLIC
{compile_opts_str}
)
# ESPHome linker options
target_link_options(${{COMPONENT_LIB}} PUBLIC
{link_opts_str}
)
"""
def write_project(minimal: bool = False) -> None:
"""Write ESP-IDF project files."""
mkdir_p(CORE.build_path)
mkdir_p(CORE.relative_src_path())
# Write top-level CMakeLists.txt
write_file_if_changed(
CORE.relative_build_path("CMakeLists.txt"),
get_project_cmakelists(),
)
# Write component CMakeLists.txt in src/
write_file_if_changed(
CORE.relative_src_path("CMakeLists.txt"),
get_component_cmakelists(minimal=minimal),
)

View File

@@ -69,6 +69,7 @@ from esphome.cpp_types import ( # noqa: F401
JsonObjectConst,
Parented,
PollingComponent,
StringRef,
arduino_json_ns,
bool_,
const_char_ptr,

View File

@@ -1,21 +1,12 @@
#include "am43_base.h"
#include "esphome/core/helpers.h"
#include <cstring>
#include <cstdio>
namespace esphome {
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];
memset(buf, 0, 64);
for (int i = 0; i < len; i++)
sprintf(&buf[i * 2], "%02x", data[i]);
std::string ret = buf;
return ret;
}
Am43Packet *Am43Encoder::get_battery_level_request() {
uint8_t data = 0x1;
return this->encode_(0xA2, &data, 1);
@@ -73,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_;
}
@@ -88,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;
sprintf((char *) this->packet_.data, "%s", CMD_READ_DEVICE_STATUS);
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;
sprintf((char *) this->packet_.data, "%s", CMD_READ_TARGET_TEMP);
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;
sprintf((char *) this->packet_.data, "%s", CMD_READ_CURRENT_TEMP);
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;
sprintf((char *) this->packet_.data, "%s", CMD_READ_UNIT);
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;
sprintf((char *) this->packet_.data, "%s", CMD_READ_DATA);
snprintf((char *) this->packet_.data, sizeof(this->packet_.data), "%s", CMD_READ_DATA);
return this->clean_packet_();
}
@@ -50,25 +50,25 @@ AnovaPacket *AnovaCodec::get_set_target_temp_request(float temperature) {
this->current_query_ = SET_TARGET_TEMPERATURE;
if (this->fahrenheit_)
temperature = ctof(temperature);
sprintf((char *) this->packet_.data, CMD_SET_TARGET_TEMP, temperature);
snprintf((char *) this->packet_.data, sizeof(this->packet_.data), CMD_SET_TARGET_TEMP, temperature);
return this->clean_packet_();
}
AnovaPacket *AnovaCodec::get_set_unit_request(char unit) {
this->current_query_ = SET_UNIT;
sprintf((char *) this->packet_.data, CMD_SET_TEMP_UNIT, unit);
snprintf((char *) this->packet_.data, sizeof(this->packet_.data), CMD_SET_TEMP_UNIT, unit);
return this->clean_packet_();
}
AnovaPacket *AnovaCodec::get_start_request() {
this->current_query_ = START;
sprintf((char *) this->packet_.data, CMD_START);
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;
sprintf((char *) this->packet_.data, CMD_STOP);
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

@@ -135,8 +135,8 @@ void BluetoothConnection::loop() {
// - For V3_WITH_CACHE: Services are never sent, disable after INIT state
// - For V3_WITHOUT_CACHE: Disable only after service discovery is complete
// (send_service_ == DONE_SENDING_SERVICES, which is only set after services are sent)
if (this->state_ != espbt::ClientState::INIT && (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
this->send_service_ == DONE_SENDING_SERVICES)) {
if (this->state() != espbt::ClientState::INIT && (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
this->send_service_ == DONE_SENDING_SERVICES)) {
this->disable_loop();
}
}

View File

@@ -152,6 +152,13 @@ void CC1101Component::setup() {
}
}
void CC1101Component::call_listeners_(const std::vector<uint8_t> &packet, float freq_offset, float rssi, uint8_t lqi) {
for (auto &listener : this->listeners_) {
listener->on_packet(packet, freq_offset, rssi, lqi);
}
this->packet_trigger_->trigger(packet, freq_offset, rssi, lqi);
}
void CC1101Component::loop() {
if (this->state_.PKT_FORMAT != static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO) || this->gdo0_pin_ == nullptr ||
!this->gdo0_pin_->digital_read()) {
@@ -198,7 +205,7 @@ void CC1101Component::loop() {
bool crc_ok = (this->state_.LQI & STATUS_CRC_OK_MASK) != 0;
uint8_t lqi = this->state_.LQI & STATUS_LQI_MASK;
if (this->state_.CRC_EN == 0 || crc_ok) {
this->packet_trigger_->trigger(this->packet_, freq_offset, rssi, lqi);
this->call_listeners_(this->packet_, freq_offset, rssi, lqi);
}
// Return to rx

View File

@@ -11,6 +11,11 @@ namespace esphome::cc1101 {
enum class CC1101Error { NONE = 0, TIMEOUT, PARAMS, CRC_ERROR, FIFO_OVERFLOW, PLL_LOCK };
class CC1101Listener {
public:
virtual void on_packet(const std::vector<uint8_t> &packet, float freq_offset, float rssi, uint8_t lqi) = 0;
};
class CC1101Component : public Component,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_1MHZ> {
@@ -73,6 +78,7 @@ class CC1101Component : public Component,
// Packet mode operations
CC1101Error transmit_packet(const std::vector<uint8_t> &packet);
void register_listener(CC1101Listener *listener) { this->listeners_.push_back(listener); }
Trigger<std::vector<uint8_t>, float, float, uint8_t> *get_packet_trigger() const { return this->packet_trigger_; }
protected:
@@ -89,9 +95,11 @@ class CC1101Component : public Component,
InternalGPIOPin *gdo0_pin_{nullptr};
// Packet handling
void call_listeners_(const std::vector<uint8_t> &packet, float freq_offset, float rssi, uint8_t lqi);
Trigger<std::vector<uint8_t>, float, float, uint8_t> *packet_trigger_{
new Trigger<std::vector<uint8_t>, float, float, uint8_t>()};
std::vector<uint8_t> packet_;
std::vector<CC1101Listener *> listeners_;
// Low-level Helpers
uint8_t strobe_(Command cmd);

View File

@@ -81,8 +81,8 @@ void CCS811Component::setup() {
bootloader_version, application_version);
if (this->version_ != nullptr) {
char version[20]; // "15.15.15 (0xffff)" is 17 chars, plus NUL, plus wiggle room
sprintf(version, "%d.%d.%d (0x%02x)", (application_version >> 12 & 15), (application_version >> 8 & 15),
(application_version >> 4 & 15), application_version);
buf_append_printf(version, sizeof(version), 0, "%d.%d.%d (0x%02x)", (application_version >> 12 & 15),
(application_version >> 8 & 15), (application_version >> 4 & 15), application_version);
ESP_LOGD(TAG, "publishing version state: %s", version);
this->version_->publish_state(version);
}

View File

@@ -133,7 +133,7 @@ bool CH422GGPIOPin::digital_read() { return this->parent_->digital_read(this->pi
void CH422GGPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value ^ this->inverted_); }
size_t CH422GGPIOPin::dump_summary(char *buffer, size_t len) const {
return snprintf(buffer, len, "EXIO%u via CH422G", this->pin_);
return buf_append_printf(buffer, len, 0, "EXIO%u via CH422G", this->pin_);
}
void CH422GGPIOPin::set_flags(gpio::Flags flags) {
flags_ = flags;

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

@@ -207,20 +207,24 @@ void CSE7766Component::parse_data_() {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
{
std::string buf = "Parsed:";
// Buffer: 7 + 15 + 33 + 15 + 25 = 95 chars max + null, rounded to 128 for safety margin.
// Float sizes with %.4f can be up to 11 chars for large values (e.g., 999999.9999).
char buf[128];
size_t pos = buf_append_printf(buf, sizeof(buf), 0, "Parsed:");
if (have_voltage) {
buf += str_sprintf(" V=%fV", voltage);
pos = buf_append_printf(buf, sizeof(buf), pos, " V=%.4fV", voltage);
}
if (have_current) {
buf += str_sprintf(" I=%fmA (~%fmA)", current * 1000.0f, calculated_current * 1000.0f);
pos = buf_append_printf(buf, sizeof(buf), pos, " I=%.4fmA (~%.4fmA)", current * 1000.0f,
calculated_current * 1000.0f);
}
if (have_power) {
buf += str_sprintf(" P=%fW", power);
pos = buf_append_printf(buf, sizeof(buf), pos, " P=%.4fW", power);
}
if (energy != 0.0f) {
buf += str_sprintf(" E=%fkWh (%u)", energy, cf_pulses);
buf_append_printf(buf, sizeof(buf), pos, " E=%.4fkWh (%u)", energy, cf_pulses);
}
ESP_LOGVV(TAG, "%s", buf.c_str());
ESP_LOGVV(TAG, "%s", buf);
}
#endif
}

View File

@@ -258,8 +258,9 @@ bool DaikinArcClimate::parse_state_frame_(const uint8_t frame[]) {
}
char buf[DAIKIN_STATE_FRAME_SIZE * 3 + 1] = {0};
size_t pos = 0;
for (size_t i = 0; i < DAIKIN_STATE_FRAME_SIZE; i++) {
sprintf(buf, "%s%02x ", buf, frame[i]);
pos = buf_append_printf(buf, sizeof(buf), pos, "%02x ", frame[i]);
}
ESP_LOGD(TAG, "FRAME %s", buf);
@@ -349,8 +350,9 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) {
if (data.expect_item(DAIKIN_HEADER_MARK, DAIKIN_HEADER_SPACE)) {
valid_daikin_frame = true;
size_t bytes_count = data.size() / 2 / 8;
std::unique_ptr<char[]> buf(new char[bytes_count * 3 + 1]);
buf[0] = '\0';
size_t buf_size = bytes_count * 3 + 1;
std::unique_ptr<char[]> buf(new char[buf_size]()); // value-initialize (zero-fill)
size_t buf_pos = 0;
for (size_t i = 0; i < bytes_count; i++) {
uint8_t byte = 0;
for (int8_t bit = 0; bit < 8; bit++) {
@@ -361,19 +363,19 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) {
break;
}
}
sprintf(buf.get(), "%s%02x ", buf.get(), byte);
buf_pos = buf_append_printf(buf.get(), buf_size, buf_pos, "%02x ", byte);
}
ESP_LOGD(TAG, "WHOLE FRAME %s size: %d", buf.get(), data.size());
}
if (!valid_daikin_frame) {
char sbuf[16 * 10 + 1];
sbuf[0] = '\0';
char sbuf[16 * 10 + 1] = {0};
size_t sbuf_pos = 0;
for (size_t j = 0; j < static_cast<size_t>(data.size()); j++) {
if ((j - 2) % 16 == 0) {
if (j > 0) {
ESP_LOGD(TAG, "DATA %04x: %s", (j - 16 > 0xffff ? 0 : j - 16), sbuf);
}
sbuf[0] = '\0';
sbuf_pos = 0;
}
char type_ch = ' ';
// debug_tolerance = 25%
@@ -401,9 +403,10 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) {
type_ch = '0';
if (abs(data[j]) > 100000) {
sprintf(sbuf, "%s%-5d[%c] ", sbuf, data[j] > 0 ? 99999 : -99999, type_ch);
sbuf_pos = buf_append_printf(sbuf, sizeof(sbuf), sbuf_pos, "%-5d[%c] ", data[j] > 0 ? 99999 : -99999, type_ch);
} else {
sprintf(sbuf, "%s%-5d[%c] ", sbuf, (int) (round(data[j] / 10.) * 10), type_ch);
sbuf_pos =
buf_append_printf(sbuf, sizeof(sbuf), sbuf_pos, "%-5d[%c] ", (int) (round(data[j] / 10.) * 10), type_ch);
}
if (j + 1 == static_cast<size_t>(data.size())) {
ESP_LOGD(TAG, "DATA %04x: %s", (j - 8 > 0xffff ? 0 : j - 8), sbuf);

View File

@@ -106,9 +106,9 @@ DateCall &DateCall::set_date(uint16_t year, uint8_t month, uint8_t day) {
DateCall &DateCall::set_date(ESPTime time) { return this->set_date(time.year, time.month, time.day_of_month); };
DateCall &DateCall::set_date(const std::string &date) {
DateCall &DateCall::set_date(const char *date, size_t len) {
ESPTime val{};
if (!ESPTime::strptime(date, val)) {
if (!ESPTime::strptime(date, len, val)) {
ESP_LOGE(TAG, "Could not convert the date string to an ESPTime object");
return *this;
}

View File

@@ -67,7 +67,9 @@ class DateCall {
void perform();
DateCall &set_date(uint16_t year, uint8_t month, uint8_t day);
DateCall &set_date(ESPTime time);
DateCall &set_date(const std::string &date);
DateCall &set_date(const char *date, size_t len);
DateCall &set_date(const char *date) { return this->set_date(date, strlen(date)); }
DateCall &set_date(const std::string &date) { return this->set_date(date.c_str(), date.size()); }
DateCall &set_year(uint16_t year) {
this->year_ = year;

View File

@@ -163,9 +163,9 @@ DateTimeCall &DateTimeCall::set_datetime(ESPTime datetime) {
datetime.second);
};
DateTimeCall &DateTimeCall::set_datetime(const std::string &datetime) {
DateTimeCall &DateTimeCall::set_datetime(const char *datetime, size_t len) {
ESPTime val{};
if (!ESPTime::strptime(datetime, val)) {
if (!ESPTime::strptime(datetime, len, val)) {
ESP_LOGE(TAG, "Could not convert the time string to an ESPTime object");
return *this;
}

View File

@@ -71,7 +71,11 @@ class DateTimeCall {
void perform();
DateTimeCall &set_datetime(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, uint8_t second);
DateTimeCall &set_datetime(ESPTime datetime);
DateTimeCall &set_datetime(const std::string &datetime);
DateTimeCall &set_datetime(const char *datetime, size_t len);
DateTimeCall &set_datetime(const char *datetime) { return this->set_datetime(datetime, strlen(datetime)); }
DateTimeCall &set_datetime(const std::string &datetime) {
return this->set_datetime(datetime.c_str(), datetime.size());
}
DateTimeCall &set_datetime(time_t epoch_seconds);
DateTimeCall &set_year(uint16_t year) {

View File

@@ -74,9 +74,9 @@ TimeCall &TimeCall::set_time(uint8_t hour, uint8_t minute, uint8_t second) {
TimeCall &TimeCall::set_time(ESPTime time) { return this->set_time(time.hour, time.minute, time.second); };
TimeCall &TimeCall::set_time(const std::string &time) {
TimeCall &TimeCall::set_time(const char *time, size_t len) {
ESPTime val{};
if (!ESPTime::strptime(time, val)) {
if (!ESPTime::strptime(time, len, val)) {
ESP_LOGE(TAG, "Could not convert the time string to an ESPTime object");
return *this;
}

View File

@@ -69,7 +69,9 @@ class TimeCall {
void perform();
TimeCall &set_time(uint8_t hour, uint8_t minute, uint8_t second);
TimeCall &set_time(ESPTime time);
TimeCall &set_time(const std::string &time);
TimeCall &set_time(const char *time, size_t len);
TimeCall &set_time(const char *time) { return this->set_time(time, strlen(time)); }
TimeCall &set_time(const std::string &time) { return this->set_time(time.c_str(), time.size()); }
TimeCall &set_hour(uint8_t hour) {
this->hour_ = hour;

View File

@@ -30,7 +30,7 @@ void DebugComponent::dump_config() {
char device_info_buffer[DEVICE_INFO_BUFFER_SIZE];
ESP_LOGD(TAG, "ESPHome version %s", ESPHOME_VERSION);
size_t pos = buf_append(device_info_buffer, DEVICE_INFO_BUFFER_SIZE, 0, "%s", ESPHOME_VERSION);
size_t pos = buf_append_printf(device_info_buffer, DEVICE_INFO_BUFFER_SIZE, 0, "%s", ESPHOME_VERSION);
this->free_heap_ = get_free_heap_();
ESP_LOGD(TAG, "Free Heap Size: %" PRIu32 " bytes", this->free_heap_);

View File

@@ -5,12 +5,6 @@
#include "esphome/core/helpers.h"
#include "esphome/core/macros.h"
#include <span>
#include <cstdarg>
#include <cstdio>
#include <algorithm>
#ifdef USE_ESP8266
#include <pgmspace.h>
#endif
#ifdef USE_SENSOR
#include "esphome/components/sensor/sensor.h"
@@ -25,40 +19,7 @@ namespace debug {
static constexpr size_t DEVICE_INFO_BUFFER_SIZE = 256;
static constexpr size_t RESET_REASON_BUFFER_SIZE = 128;
#ifdef USE_ESP8266
// ESP8266: Use vsnprintf_P to keep format strings in flash (PROGMEM)
// Format strings must be wrapped with PSTR() macro
inline size_t buf_append_p(char *buf, size_t size, size_t pos, PGM_P fmt, ...) {
if (pos >= size) {
return size;
}
va_list args;
va_start(args, fmt);
int written = vsnprintf_P(buf + pos, size - pos, fmt, args);
va_end(args);
if (written < 0) {
return pos; // encoding error
}
return std::min(pos + static_cast<size_t>(written), size);
}
#define buf_append(buf, size, pos, fmt, ...) buf_append_p(buf, size, pos, PSTR(fmt), ##__VA_ARGS__)
#else
/// Safely append formatted string to buffer, returning new position (capped at size)
__attribute__((format(printf, 4, 5))) inline size_t buf_append(char *buf, size_t size, size_t pos, const char *fmt,
...) {
if (pos >= size) {
return size;
}
va_list args;
va_start(args, fmt);
int written = vsnprintf(buf + pos, size - pos, fmt, args);
va_end(args);
if (written < 0) {
return pos; // encoding error
}
return std::min(pos + static_cast<size_t>(written), size);
}
#endif
// buf_append_printf is now provided by esphome/core/helpers.h
class DebugComponent : public PollingComponent {
public:

View File

@@ -173,8 +173,8 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
uint32_t flash_size = ESP.getFlashChipSize() / 1024; // NOLINT
uint32_t flash_speed = ESP.getFlashChipSpeed() / 1000000; // NOLINT
ESP_LOGD(TAG, "Flash Chip: Size=%" PRIu32 "kB Speed=%" PRIu32 "MHz Mode=%s", flash_size, flash_speed, flash_mode);
pos = buf_append(buf, size, pos, "|Flash: %" PRIu32 "kB Speed:%" PRIu32 "MHz Mode:%s", flash_size, flash_speed,
flash_mode);
pos = buf_append_printf(buf, size, pos, "|Flash: %" PRIu32 "kB Speed:%" PRIu32 "MHz Mode:%s", flash_size, flash_speed,
flash_mode);
#endif
esp_chip_info_t info;
@@ -182,52 +182,52 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
const char *model = ESPHOME_VARIANT;
// Build features string
pos = buf_append(buf, size, pos, "|Chip: %s Features:", model);
pos = buf_append_printf(buf, size, pos, "|Chip: %s Features:", model);
bool first_feature = true;
for (const auto &feature : CHIP_FEATURES) {
if (info.features & feature.bit) {
pos = buf_append(buf, size, pos, "%s%s", first_feature ? "" : ", ", feature.name);
pos = buf_append_printf(buf, size, pos, "%s%s", first_feature ? "" : ", ", feature.name);
first_feature = false;
info.features &= ~feature.bit;
}
}
if (info.features != 0) {
pos = buf_append(buf, size, pos, "%sOther:0x%" PRIx32, first_feature ? "" : ", ", info.features);
pos = buf_append_printf(buf, size, pos, "%sOther:0x%" PRIx32, first_feature ? "" : ", ", info.features);
}
ESP_LOGD(TAG, "Chip: Model=%s, Cores=%u, Revision=%u", model, info.cores, info.revision);
pos = buf_append(buf, size, pos, " Cores:%u Revision:%u", info.cores, info.revision);
pos = buf_append_printf(buf, size, pos, " Cores:%u Revision:%u", info.cores, info.revision);
uint32_t cpu_freq_mhz = arch_get_cpu_freq_hz() / 1000000;
ESP_LOGD(TAG, "CPU Frequency: %" PRIu32 " MHz", cpu_freq_mhz);
pos = buf_append(buf, size, pos, "|CPU Frequency: %" PRIu32 " MHz", cpu_freq_mhz);
pos = buf_append_printf(buf, size, pos, "|CPU Frequency: %" PRIu32 " MHz", cpu_freq_mhz);
// Framework detection
#ifdef USE_ARDUINO
ESP_LOGD(TAG, "Framework: Arduino");
pos = buf_append(buf, size, pos, "|Framework: Arduino");
pos = buf_append_printf(buf, size, pos, "|Framework: Arduino");
#elif defined(USE_ESP32)
ESP_LOGD(TAG, "Framework: ESP-IDF");
pos = buf_append(buf, size, pos, "|Framework: ESP-IDF");
pos = buf_append_printf(buf, size, pos, "|Framework: ESP-IDF");
#else
ESP_LOGW(TAG, "Framework: UNKNOWN");
pos = buf_append(buf, size, pos, "|Framework: UNKNOWN");
pos = buf_append_printf(buf, size, pos, "|Framework: UNKNOWN");
#endif
ESP_LOGD(TAG, "ESP-IDF Version: %s", esp_get_idf_version());
pos = buf_append(buf, size, pos, "|ESP-IDF: %s", esp_get_idf_version());
pos = buf_append_printf(buf, size, pos, "|ESP-IDF: %s", esp_get_idf_version());
uint8_t mac[6];
get_mac_address_raw(mac);
ESP_LOGD(TAG, "EFuse MAC: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
pos = buf_append(buf, size, pos, "|EFuse MAC: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4],
mac[5]);
pos = buf_append_printf(buf, size, pos, "|EFuse MAC: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3],
mac[4], mac[5]);
char reason_buffer[RESET_REASON_BUFFER_SIZE];
const char *reset_reason = get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE>(reason_buffer));
pos = buf_append(buf, size, pos, "|Reset: %s", reset_reason);
pos = buf_append_printf(buf, size, pos, "|Reset: %s", reset_reason);
const char *wakeup_cause = get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE>(reason_buffer));
pos = buf_append(buf, size, pos, "|Wakeup: %s", wakeup_cause);
pos = buf_append_printf(buf, size, pos, "|Wakeup: %s", wakeup_cause);
return pos;
}

View File

@@ -3,21 +3,80 @@
#include "esphome/core/log.h"
#include <Esp.h>
extern "C" {
#include <user_interface.h>
// Global reset info struct populated by SDK at boot
extern struct rst_info resetInfo;
// Core version - either a string pointer or a version number to format as hex
extern uint32_t core_version;
extern const char *core_release;
}
namespace esphome {
namespace debug {
static const char *const TAG = "debug";
// Get reset reason string from reason code (no heap allocation)
// Returns LogString* pointing to flash (PROGMEM) on ESP8266
static const LogString *get_reset_reason_str(uint32_t reason) {
switch (reason) {
case REASON_DEFAULT_RST:
return LOG_STR("Power On");
case REASON_WDT_RST:
return LOG_STR("Hardware Watchdog");
case REASON_EXCEPTION_RST:
return LOG_STR("Exception");
case REASON_SOFT_WDT_RST:
return LOG_STR("Software Watchdog");
case REASON_SOFT_RESTART:
return LOG_STR("Software/System restart");
case REASON_DEEP_SLEEP_AWAKE:
return LOG_STR("Deep-Sleep Wake");
case REASON_EXT_SYS_RST:
return LOG_STR("External System");
default:
return LOG_STR("Unknown");
}
}
// Size for core version hex buffer
static constexpr size_t CORE_VERSION_BUFFER_SIZE = 12;
// Get core version string (no heap allocation)
// Returns either core_release directly or formats core_version as hex into provided buffer
static const char *get_core_version_str(std::span<char, CORE_VERSION_BUFFER_SIZE> buffer) {
if (core_release != nullptr) {
return core_release;
}
snprintf_P(buffer.data(), CORE_VERSION_BUFFER_SIZE, PSTR("%08x"), core_version);
return buffer.data();
}
// Size for reset info buffer
static constexpr size_t RESET_INFO_BUFFER_SIZE = 200;
// Get detailed reset info string (no heap allocation)
// For watchdog/exception resets, includes detailed exception info
static const char *get_reset_info_str(std::span<char, RESET_INFO_BUFFER_SIZE> buffer, uint32_t reason) {
if (reason >= REASON_WDT_RST && reason <= REASON_SOFT_WDT_RST) {
snprintf_P(buffer.data(), RESET_INFO_BUFFER_SIZE,
PSTR("Fatal exception:%d flag:%d (%s) epc1:0x%08x epc2:0x%08x epc3:0x%08x excvaddr:0x%08x depc:0x%08x"),
static_cast<int>(resetInfo.exccause), static_cast<int>(reason),
LOG_STR_ARG(get_reset_reason_str(reason)), resetInfo.epc1, resetInfo.epc2, resetInfo.epc3,
resetInfo.excvaddr, resetInfo.depc);
return buffer.data();
}
return LOG_STR_ARG(get_reset_reason_str(reason));
}
const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) {
char *buf = buffer.data();
#if !defined(CLANG_TIDY)
String reason = ESP.getResetReason(); // NOLINT
snprintf_P(buf, RESET_REASON_BUFFER_SIZE, PSTR("%s"), reason.c_str());
return buf;
#else
buf[0] = '\0';
return buf;
#endif
// Copy from flash to provided buffer
strncpy_P(buffer.data(), (PGM_P) get_reset_reason_str(resetInfo.reason), RESET_REASON_BUFFER_SIZE - 1);
buffer[RESET_REASON_BUFFER_SIZE - 1] = '\0';
return buffer.data();
}
const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) {
@@ -33,37 +92,42 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
constexpr size_t size = DEVICE_INFO_BUFFER_SIZE;
char *buf = buffer.data();
const char *flash_mode;
const LogString *flash_mode;
switch (ESP.getFlashChipMode()) { // NOLINT(readability-static-accessed-through-instance)
case FM_QIO:
flash_mode = "QIO";
flash_mode = LOG_STR("QIO");
break;
case FM_QOUT:
flash_mode = "QOUT";
flash_mode = LOG_STR("QOUT");
break;
case FM_DIO:
flash_mode = "DIO";
flash_mode = LOG_STR("DIO");
break;
case FM_DOUT:
flash_mode = "DOUT";
flash_mode = LOG_STR("DOUT");
break;
default:
flash_mode = "UNKNOWN";
flash_mode = LOG_STR("UNKNOWN");
}
uint32_t flash_size = ESP.getFlashChipSize() / 1024; // NOLINT
uint32_t flash_speed = ESP.getFlashChipSpeed() / 1000000; // NOLINT
ESP_LOGD(TAG, "Flash Chip: Size=%" PRIu32 "kB Speed=%" PRIu32 "MHz Mode=%s", flash_size, flash_speed, flash_mode);
pos = buf_append(buf, size, pos, "|Flash: %" PRIu32 "kB Speed:%" PRIu32 "MHz Mode:%s", flash_size, flash_speed,
flash_mode);
uint32_t flash_size = ESP.getFlashChipSize() / 1024; // NOLINT(readability-static-accessed-through-instance)
uint32_t flash_speed = ESP.getFlashChipSpeed() / 1000000; // NOLINT(readability-static-accessed-through-instance)
ESP_LOGD(TAG, "Flash Chip: Size=%" PRIu32 "kB Speed=%" PRIu32 "MHz Mode=%s", flash_size, flash_speed,
LOG_STR_ARG(flash_mode));
pos = buf_append_printf(buf, size, pos, "|Flash: %" PRIu32 "kB Speed:%" PRIu32 "MHz Mode:%s", flash_size, flash_speed,
LOG_STR_ARG(flash_mode));
#if !defined(CLANG_TIDY)
char reason_buffer[RESET_REASON_BUFFER_SIZE];
const char *reset_reason = get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE>(reason_buffer));
const char *reset_reason = get_reset_reason_(reason_buffer);
char core_version_buffer[CORE_VERSION_BUFFER_SIZE];
char reset_info_buffer[RESET_INFO_BUFFER_SIZE];
// NOLINTBEGIN(readability-static-accessed-through-instance)
uint32_t chip_id = ESP.getChipId();
uint8_t boot_version = ESP.getBootVersion();
uint8_t boot_mode = ESP.getBootMode();
uint8_t cpu_freq = ESP.getCpuFreqMHz();
uint32_t flash_chip_id = ESP.getFlashChipId();
const char *sdk_version = ESP.getSdkVersion();
// NOLINTEND(readability-static-accessed-through-instance)
ESP_LOGD(TAG,
"Chip ID: 0x%08" PRIX32 "\n"
@@ -74,19 +138,18 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
"Flash Chip ID=0x%08" PRIX32 "\n"
"Reset Reason: %s\n"
"Reset Info: %s",
chip_id, ESP.getSdkVersion(), ESP.getCoreVersion().c_str(), boot_version, boot_mode, cpu_freq, flash_chip_id,
reset_reason, ESP.getResetInfo().c_str());
chip_id, sdk_version, get_core_version_str(core_version_buffer), boot_version, boot_mode, cpu_freq,
flash_chip_id, reset_reason, get_reset_info_str(reset_info_buffer, resetInfo.reason));
pos = buf_append(buf, size, pos, "|Chip: 0x%08" PRIX32, chip_id);
pos = buf_append(buf, size, pos, "|SDK: %s", ESP.getSdkVersion());
pos = buf_append(buf, size, pos, "|Core: %s", ESP.getCoreVersion().c_str());
pos = buf_append(buf, size, pos, "|Boot: %u", boot_version);
pos = buf_append(buf, size, pos, "|Mode: %u", boot_mode);
pos = buf_append(buf, size, pos, "|CPU: %u", cpu_freq);
pos = buf_append(buf, size, pos, "|Flash: 0x%08" PRIX32, flash_chip_id);
pos = buf_append(buf, size, pos, "|Reset: %s", reset_reason);
pos = buf_append(buf, size, pos, "|%s", ESP.getResetInfo().c_str());
#endif
pos = buf_append_printf(buf, size, pos, "|Chip: 0x%08" PRIX32, chip_id);
pos = buf_append_printf(buf, size, pos, "|SDK: %s", sdk_version);
pos = buf_append_printf(buf, size, pos, "|Core: %s", get_core_version_str(core_version_buffer));
pos = buf_append_printf(buf, size, pos, "|Boot: %u", boot_version);
pos = buf_append_printf(buf, size, pos, "|Mode: %u", boot_mode);
pos = buf_append_printf(buf, size, pos, "|CPU: %u", cpu_freq);
pos = buf_append_printf(buf, size, pos, "|Flash: 0x%08" PRIX32, flash_chip_id);
pos = buf_append_printf(buf, size, pos, "|Reset: %s", reset_reason);
pos = buf_append_printf(buf, size, pos, "|%s", get_reset_info_str(reset_info_buffer, resetInfo.reason));
return pos;
}

View File

@@ -36,12 +36,12 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
lt_get_version(), lt_cpu_get_model_name(), lt_cpu_get_model(), lt_cpu_get_freq_mhz(), mac_id,
lt_get_board_code(), flash_kib, ram_kib, reset_reason);
pos = buf_append(buf, size, pos, "|Version: %s", LT_BANNER_STR + 10);
pos = buf_append(buf, size, pos, "|Reset Reason: %s", reset_reason);
pos = buf_append(buf, size, pos, "|Chip Name: %s", lt_cpu_get_model_name());
pos = buf_append(buf, size, pos, "|Chip ID: 0x%06" PRIX32, mac_id);
pos = buf_append(buf, size, pos, "|Flash: %" PRIu32 " KiB", flash_kib);
pos = buf_append(buf, size, pos, "|RAM: %" PRIu32 " KiB", ram_kib);
pos = buf_append_printf(buf, size, pos, "|Version: %s", LT_BANNER_STR + 10);
pos = buf_append_printf(buf, size, pos, "|Reset Reason: %s", reset_reason);
pos = buf_append_printf(buf, size, pos, "|Chip Name: %s", lt_cpu_get_model_name());
pos = buf_append_printf(buf, size, pos, "|Chip ID: 0x%06" PRIX32, mac_id);
pos = buf_append_printf(buf, size, pos, "|Flash: %" PRIu32 " KiB", flash_kib);
pos = buf_append_printf(buf, size, pos, "|RAM: %" PRIu32 " KiB", ram_kib);
return pos;
}

View File

@@ -19,7 +19,7 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
uint32_t cpu_freq = rp2040.f_cpu();
ESP_LOGD(TAG, "CPU Frequency: %" PRIu32, cpu_freq);
pos = buf_append(buf, size, pos, "|CPU Frequency: %" PRIu32, cpu_freq);
pos = buf_append_printf(buf, size, pos, "|CPU Frequency: %" PRIu32, cpu_freq);
return pos;
}

View File

@@ -20,9 +20,9 @@ static size_t append_reset_reason(char *buf, size_t size, size_t pos, bool set,
return pos;
}
if (pos > 0) {
pos = buf_append(buf, size, pos, ", ");
pos = buf_append_printf(buf, size, pos, ", ");
}
return buf_append(buf, size, pos, "%s", reason);
return buf_append_printf(buf, size, pos, "%s", reason);
}
static inline uint32_t read_mem_u32(uintptr_t addr) {
@@ -140,7 +140,7 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
const char *supply_status =
(nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_NORMAL) ? "Normal voltage." : "High voltage.";
ESP_LOGD(TAG, "Main supply status: %s", supply_status);
pos = buf_append(buf, size, pos, "|Main supply status: %s", supply_status);
pos = buf_append_printf(buf, size, pos, "|Main supply status: %s", supply_status);
// Regulator stage 0
if (nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_HIGH) {
@@ -172,16 +172,16 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
reg0_voltage = "???V";
}
ESP_LOGD(TAG, "Regulator stage 0: %s, %s", reg0_type, reg0_voltage);
pos = buf_append(buf, size, pos, "|Regulator stage 0: %s, %s", reg0_type, reg0_voltage);
pos = buf_append_printf(buf, size, pos, "|Regulator stage 0: %s, %s", reg0_type, reg0_voltage);
} else {
ESP_LOGD(TAG, "Regulator stage 0: disabled");
pos = buf_append(buf, size, pos, "|Regulator stage 0: disabled");
pos = buf_append_printf(buf, size, pos, "|Regulator stage 0: disabled");
}
// Regulator stage 1
const char *reg1_type = nrf_power_dcdcen_get(NRF_POWER) ? "DC/DC" : "LDO";
ESP_LOGD(TAG, "Regulator stage 1: %s", reg1_type);
pos = buf_append(buf, size, pos, "|Regulator stage 1: %s", reg1_type);
pos = buf_append_printf(buf, size, pos, "|Regulator stage 1: %s", reg1_type);
// USB power state
const char *usb_state;
@@ -195,7 +195,7 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
usb_state = "disconnected";
}
ESP_LOGD(TAG, "USB power state: %s", usb_state);
pos = buf_append(buf, size, pos, "|USB power state: %s", usb_state);
pos = buf_append_printf(buf, size, pos, "|USB power state: %s", usb_state);
// Power-fail comparator
bool enabled;
@@ -300,14 +300,14 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
break;
}
ESP_LOGD(TAG, "Power-fail comparator: %s, VDDH: %s", pof_voltage, vddh_voltage);
pos = buf_append(buf, size, pos, "|Power-fail comparator: %s, VDDH: %s", pof_voltage, vddh_voltage);
pos = buf_append_printf(buf, size, pos, "|Power-fail comparator: %s, VDDH: %s", pof_voltage, vddh_voltage);
} else {
ESP_LOGD(TAG, "Power-fail comparator: %s", pof_voltage);
pos = buf_append(buf, size, pos, "|Power-fail comparator: %s", pof_voltage);
pos = buf_append_printf(buf, size, pos, "|Power-fail comparator: %s", pof_voltage);
}
} else {
ESP_LOGD(TAG, "Power-fail comparator: disabled");
pos = buf_append(buf, size, pos, "|Power-fail comparator: disabled");
pos = buf_append_printf(buf, size, pos, "|Power-fail comparator: disabled");
}
auto package = [](uint32_t value) {

View File

@@ -127,7 +127,9 @@ DetRangeCfgCommand::DetRangeCfgCommand(float min1, float max1, float min2, float
this->min2_ = min2 = this->max2_ = max2 = this->min3_ = min3 = this->max3_ = max3 = this->min4_ = min4 =
this->max4_ = max4 = -1;
this->cmd_ = str_sprintf("detRangeCfg -1 %.0f %.0f", min1 / 0.15, max1 / 0.15);
char buf[72]; // max 72: "detRangeCfg -1 "(15) + 8 * (float(5) + space(1)) + null
snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f", min1 / 0.15, max1 / 0.15);
this->cmd_ = buf;
} else if (min3 < 0 || max3 < 0) {
this->min1_ = min1 = round(min1 / 0.15) * 0.15;
this->max1_ = max1 = round(max1 / 0.15) * 0.15;
@@ -135,7 +137,10 @@ DetRangeCfgCommand::DetRangeCfgCommand(float min1, float max1, float min2, float
this->max2_ = max2 = round(max2 / 0.15) * 0.15;
this->min3_ = min3 = this->max3_ = max3 = this->min4_ = min4 = this->max4_ = max4 = -1;
this->cmd_ = str_sprintf("detRangeCfg -1 %.0f %.0f %.0f %.0f", min1 / 0.15, max1 / 0.15, min2 / 0.15, max2 / 0.15);
char buf[72]; // max 72: "detRangeCfg -1 "(15) + 8 * (float(5) + space(1)) + null
snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f %.0f %.0f", min1 / 0.15, max1 / 0.15, min2 / 0.15,
max2 / 0.15);
this->cmd_ = buf;
} else if (min4 < 0 || max4 < 0) {
this->min1_ = min1 = round(min1 / 0.15) * 0.15;
this->max1_ = max1 = round(max1 / 0.15) * 0.15;
@@ -145,9 +150,10 @@ DetRangeCfgCommand::DetRangeCfgCommand(float min1, float max1, float min2, float
this->max3_ = max3 = round(max3 / 0.15) * 0.15;
this->min4_ = min4 = this->max4_ = max4 = -1;
this->cmd_ = str_sprintf("detRangeCfg -1 "
"%.0f %.0f %.0f %.0f %.0f %.0f",
min1 / 0.15, max1 / 0.15, min2 / 0.15, max2 / 0.15, min3 / 0.15, max3 / 0.15);
char buf[72]; // max 72: "detRangeCfg -1 "(15) + 8 * (float(5) + space(1)) + null
snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f %.0f %.0f %.0f %.0f", min1 / 0.15, max1 / 0.15, min2 / 0.15,
max2 / 0.15, min3 / 0.15, max3 / 0.15);
this->cmd_ = buf;
} else {
this->min1_ = min1 = round(min1 / 0.15) * 0.15;
this->max1_ = max1 = round(max1 / 0.15) * 0.15;
@@ -158,10 +164,10 @@ DetRangeCfgCommand::DetRangeCfgCommand(float min1, float max1, float min2, float
this->min4_ = min4 = round(min4 / 0.15) * 0.15;
this->max4_ = max4 = round(max4 / 0.15) * 0.15;
this->cmd_ = str_sprintf("detRangeCfg -1 "
"%.0f %.0f %.0f %.0f %.0f %.0f %.0f %.0f",
min1 / 0.15, max1 / 0.15, min2 / 0.15, max2 / 0.15, min3 / 0.15, max3 / 0.15, min4 / 0.15,
max4 / 0.15);
char buf[72]; // max 72: "detRangeCfg -1 "(15) + 8 * (float(5) + space(1)) + null
snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f %.0f %.0f %.0f %.0f %.0f %.0f", min1 / 0.15, max1 / 0.15,
min2 / 0.15, max2 / 0.15, min3 / 0.15, max3 / 0.15, min4 / 0.15, max4 / 0.15);
this->cmd_ = buf;
}
this->min1_ = min1;
@@ -203,7 +209,10 @@ SetLatencyCommand::SetLatencyCommand(float delay_after_detection, float delay_af
delay_after_disappear = std::round(delay_after_disappear / 0.025f) * 0.025f;
this->delay_after_detection_ = clamp(delay_after_detection, 0.0f, 1638.375f);
this->delay_after_disappear_ = clamp(delay_after_disappear, 0.0f, 1638.375f);
this->cmd_ = str_sprintf("setLatency %.03f %.03f", this->delay_after_detection_, this->delay_after_disappear_);
// max 32: "setLatency "(11) + float(8) + " "(1) + float(8) + null, rounded to 32
char buf[32];
snprintf(buf, sizeof(buf), "setLatency %.03f %.03f", this->delay_after_detection_, this->delay_after_disappear_);
this->cmd_ = buf;
};
uint8_t SetLatencyCommand::on_message(std::string &message) {

View File

@@ -75,8 +75,8 @@ class SetLatencyCommand : public Command {
class SensorCfgStartCommand : public Command {
public:
SensorCfgStartCommand(bool startup_mode) : startup_mode_(startup_mode) {
char tmp_cmd[20] = {0};
sprintf(tmp_cmd, "sensorCfgStart %d", startup_mode);
char tmp_cmd[20]; // "sensorCfgStart " (15) + "0/1" (1) + null = 17
buf_append_printf(tmp_cmd, sizeof(tmp_cmd), 0, "sensorCfgStart %d", startup_mode);
cmd_ = std::string(tmp_cmd);
}
uint8_t on_message(std::string &message) override;
@@ -142,8 +142,8 @@ class SensitivityCommand : public Command {
SensitivityCommand(uint8_t sensitivity) : sensitivity_(sensitivity) {
if (sensitivity > 9)
sensitivity_ = sensitivity = 9;
char tmp_cmd[20] = {0};
sprintf(tmp_cmd, "setSensitivity %d", sensitivity);
char tmp_cmd[20]; // "setSensitivity " (15) + "0-9" (1) + null = 17
buf_append_printf(tmp_cmd, sizeof(tmp_cmd), 0, "setSensitivity %d", sensitivity);
cmd_ = std::string(tmp_cmd);
};
uint8_t on_message(std::string &message) override;

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

@@ -34,6 +34,7 @@ from esphome.const import (
KEY_CORE,
KEY_FRAMEWORK_VERSION,
KEY_NAME,
KEY_NATIVE_IDF,
KEY_TARGET_FRAMEWORK,
KEY_TARGET_PLATFORM,
PLATFORM_ESP32,
@@ -53,6 +54,7 @@ from .const import ( # noqa
KEY_COMPONENTS,
KEY_ESP32,
KEY_EXTRA_BUILD_FILES,
KEY_FLASH_SIZE,
KEY_PATH,
KEY_REF,
KEY_REPO,
@@ -199,6 +201,7 @@ def set_core_data(config):
)
CORE.data[KEY_ESP32][KEY_BOARD] = config[CONF_BOARD]
CORE.data[KEY_ESP32][KEY_FLASH_SIZE] = config[CONF_FLASH_SIZE]
CORE.data[KEY_ESP32][KEY_VARIANT] = variant
CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES] = {}
@@ -962,12 +965,54 @@ async def _add_yaml_idf_components(components: list[ConfigType]):
async def to_code(config):
cg.add_platformio_option("board", config[CONF_BOARD])
cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE])
cg.add_platformio_option(
"board_upload.maximum_size",
int(config[CONF_FLASH_SIZE].removesuffix("MB")) * 1024 * 1024,
)
framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
conf = config[CONF_FRAMEWORK]
# Check if using native ESP-IDF build (--native-idf)
use_platformio = not CORE.data.get(KEY_NATIVE_IDF, False)
if use_platformio:
# Clear IDF environment variables to avoid conflicts with PlatformIO's ESP-IDF
# but keep them when using --native-idf for native ESP-IDF builds
for clean_var in ("IDF_PATH", "IDF_TOOLS_PATH"):
os.environ.pop(clean_var, None)
cg.add_platformio_option("lib_ldf_mode", "off")
cg.add_platformio_option("lib_compat_mode", "strict")
cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION])
cg.add_platformio_option("board", config[CONF_BOARD])
cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE])
cg.add_platformio_option(
"board_upload.maximum_size",
int(config[CONF_FLASH_SIZE].removesuffix("MB")) * 1024 * 1024,
)
if CONF_SOURCE in conf:
cg.add_platformio_option("platform_packages", [conf[CONF_SOURCE]])
add_extra_script(
"pre",
"pre_build.py",
Path(__file__).parent / "pre_build.py.script",
)
add_extra_script(
"post",
"post_build.py",
Path(__file__).parent / "post_build.py.script",
)
# In testing mode, add IRAM fix script to allow linking grouped component tests
# Similar to ESP8266's approach but for ESP-IDF
if CORE.testing_mode:
cg.add_build_flag("-DESPHOME_TESTING_MODE")
add_extra_script(
"pre",
"iram_fix.py",
Path(__file__).parent / "iram_fix.py.script",
)
else:
cg.add_build_flag("-Wno-error=format")
cg.set_cpp_standard("gnu++20")
cg.add_build_flag("-DUSE_ESP32")
cg.add_build_flag("-Wl,-z,noexecstack")
@@ -977,79 +1022,49 @@ async def to_code(config):
cg.add_define("ESPHOME_VARIANT", VARIANT_FRIENDLY[variant])
cg.add_define(ThreadModel.MULTI_ATOMICS)
cg.add_platformio_option("lib_ldf_mode", "off")
cg.add_platformio_option("lib_compat_mode", "strict")
framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
conf = config[CONF_FRAMEWORK]
cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION])
if CONF_SOURCE in conf:
cg.add_platformio_option("platform_packages", [conf[CONF_SOURCE]])
if conf[CONF_ADVANCED][CONF_IGNORE_EFUSE_CUSTOM_MAC]:
cg.add_define("USE_ESP32_IGNORE_EFUSE_CUSTOM_MAC")
for clean_var in ("IDF_PATH", "IDF_TOOLS_PATH"):
os.environ.pop(clean_var, None)
# Set the location of the IDF component manager cache
os.environ["IDF_COMPONENT_CACHE_PATH"] = str(
CORE.relative_internal_path(".espressif")
)
add_extra_script(
"pre",
"pre_build.py",
Path(__file__).parent / "pre_build.py.script",
)
add_extra_script(
"post",
"post_build.py",
Path(__file__).parent / "post_build.py.script",
)
# In testing mode, add IRAM fix script to allow linking grouped component tests
# Similar to ESP8266's approach but for ESP-IDF
if CORE.testing_mode:
cg.add_build_flag("-DESPHOME_TESTING_MODE")
add_extra_script(
"pre",
"iram_fix.py",
Path(__file__).parent / "iram_fix.py.script",
)
if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
cg.add_platformio_option("framework", "espidf")
cg.add_build_flag("-DUSE_ESP_IDF")
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF")
if use_platformio:
cg.add_platformio_option("framework", "espidf")
else:
cg.add_platformio_option("framework", "arduino, espidf")
cg.add_build_flag("-DUSE_ARDUINO")
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ARDUINO")
if use_platformio:
cg.add_platformio_option("framework", "arduino, espidf")
# Add IDF framework source for Arduino builds to ensure it uses the same version as
# the ESP-IDF framework
if (idf_ver := ARDUINO_IDF_VERSION_LOOKUP.get(framework_ver)) is not None:
cg.add_platformio_option(
"platform_packages",
[_format_framework_espidf_version(idf_ver, None)],
)
# ESP32-S2 Arduino: Disable USB Serial on boot to avoid TinyUSB dependency
if get_esp32_variant() == VARIANT_ESP32S2:
cg.add_build_unflag("-DARDUINO_USB_CDC_ON_BOOT=1")
cg.add_build_unflag("-DARDUINO_USB_CDC_ON_BOOT=0")
cg.add_build_flag("-DARDUINO_USB_CDC_ON_BOOT=0")
cg.add_define(
"USE_ARDUINO_VERSION_CODE",
cg.RawExpression(
f"VERSION_CODE({framework_ver.major}, {framework_ver.minor}, {framework_ver.patch})"
),
)
add_idf_sdkconfig_option("CONFIG_MBEDTLS_PSK_MODES", True)
add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True)
# Add IDF framework source for Arduino builds to ensure it uses the same version as
# the ESP-IDF framework
if (idf_ver := ARDUINO_IDF_VERSION_LOOKUP.get(framework_ver)) is not None:
cg.add_platformio_option(
"platform_packages", [_format_framework_espidf_version(idf_ver, None)]
)
# ESP32-S2 Arduino: Disable USB Serial on boot to avoid TinyUSB dependency
if get_esp32_variant() == VARIANT_ESP32S2:
cg.add_build_unflag("-DARDUINO_USB_CDC_ON_BOOT=1")
cg.add_build_unflag("-DARDUINO_USB_CDC_ON_BOOT=0")
cg.add_build_flag("-DARDUINO_USB_CDC_ON_BOOT=0")
cg.add_build_flag("-Wno-nonnull-compare")
add_idf_sdkconfig_option(f"CONFIG_IDF_TARGET_{variant}", True)
@@ -1196,7 +1211,8 @@ async def to_code(config):
"CONFIG_VFS_SUPPORT_DIR", not advanced[CONF_DISABLE_VFS_SUPPORT_DIR]
)
cg.add_platformio_option("board_build.partitions", "partitions.csv")
if use_platformio:
cg.add_platformio_option("board_build.partitions", "partitions.csv")
if CONF_PARTITIONS in config:
add_extra_build_file(
"partitions.csv", CORE.relative_config_path(config[CONF_PARTITIONS])
@@ -1361,19 +1377,16 @@ def copy_files():
_write_idf_component_yml()
if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]:
flash_size = CORE.data[KEY_ESP32][KEY_FLASH_SIZE]
if CORE.using_arduino:
write_file_if_changed(
CORE.relative_build_path("partitions.csv"),
get_arduino_partition_csv(
CORE.platformio_options.get("board_upload.flash_size")
),
get_arduino_partition_csv(flash_size),
)
else:
write_file_if_changed(
CORE.relative_build_path("partitions.csv"),
get_idf_partition_csv(
CORE.platformio_options.get("board_upload.flash_size")
),
get_idf_partition_csv(flash_size),
)
# IDF build scripts look for version string to put in the build.
# However, if the build path does not have an initialized git repo,

View File

@@ -2,6 +2,7 @@ import esphome.codegen as cg
KEY_ESP32 = "esp32"
KEY_BOARD = "board"
KEY_FLASH_SIZE = "flash_size"
KEY_VARIANT = "variant"
KEY_SDKCONFIG_OPTIONS = "sdkconfig_options"
KEY_COMPONENTS = "components"

View File

@@ -181,7 +181,8 @@ class ESP32Preferences : public ESPPreferences {
if (actual_len != to_save.len) {
return true;
}
auto stored_data = std::make_unique<uint8_t[]>(actual_len);
// Most preferences are small, use stack buffer with heap fallback for large ones
SmallBufferWithHeapFallback<256> stored_data(actual_len);
err = nvs_get_blob(nvs_handle, key_str, stored_data.get(), &actual_len);
if (err != 0) {
ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err));

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

@@ -50,7 +50,7 @@ void BLEClientBase::loop() {
this->set_state(espbt::ClientState::INIT);
return;
}
if (this->state_ == espbt::ClientState::INIT) {
if (this->state() == espbt::ClientState::INIT) {
auto ret = esp_ble_gattc_app_register(this->app_id);
if (ret) {
ESP_LOGE(TAG, "gattc app register failed. app_id=%d code=%d", this->app_id, ret);
@@ -60,7 +60,7 @@ void BLEClientBase::loop() {
}
// If idle, we can disable the loop as connect()
// will enable it again when a connection is needed.
else if (this->state_ == espbt::ClientState::IDLE) {
else if (this->state() == espbt::ClientState::IDLE) {
this->disable_loop();
}
}
@@ -86,7 +86,7 @@ bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) {
return false;
if (this->address_ == 0 || device.address_uint64() != this->address_)
return false;
if (this->state_ != espbt::ClientState::IDLE)
if (this->state() != espbt::ClientState::IDLE)
return false;
this->log_event_("Found device");
@@ -102,10 +102,10 @@ bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) {
void BLEClientBase::connect() {
// Prevent duplicate connection attempts
if (this->state_ == espbt::ClientState::CONNECTING || this->state_ == espbt::ClientState::CONNECTED ||
this->state_ == espbt::ClientState::ESTABLISHED) {
if (this->state() == espbt::ClientState::CONNECTING || this->state() == espbt::ClientState::CONNECTED ||
this->state() == espbt::ClientState::ESTABLISHED) {
ESP_LOGW(TAG, "[%d] [%s] Connection already in progress, state=%s", this->connection_index_, this->address_str_,
espbt::client_state_to_string(this->state_));
espbt::client_state_to_string(this->state()));
return;
}
ESP_LOGI(TAG, "[%d] [%s] 0x%02x Connecting", this->connection_index_, this->address_str_, this->remote_addr_type_);
@@ -133,12 +133,12 @@ void BLEClientBase::connect() {
esp_err_t BLEClientBase::pair() { return esp_ble_set_encryption(this->remote_bda_, ESP_BLE_SEC_ENCRYPT); }
void BLEClientBase::disconnect() {
if (this->state_ == espbt::ClientState::IDLE || this->state_ == espbt::ClientState::DISCONNECTING) {
if (this->state() == espbt::ClientState::IDLE || this->state() == espbt::ClientState::DISCONNECTING) {
ESP_LOGI(TAG, "[%d] [%s] Disconnect requested, but already %s", this->connection_index_, this->address_str_,
espbt::client_state_to_string(this->state_));
espbt::client_state_to_string(this->state()));
return;
}
if (this->state_ == espbt::ClientState::CONNECTING || this->conn_id_ == UNSET_CONN_ID) {
if (this->state() == espbt::ClientState::CONNECTING || this->conn_id_ == UNSET_CONN_ID) {
ESP_LOGD(TAG, "[%d] [%s] Disconnect before connected, disconnect scheduled", this->connection_index_,
this->address_str_);
this->want_disconnect_ = true;
@@ -150,7 +150,7 @@ void BLEClientBase::disconnect() {
void BLEClientBase::unconditional_disconnect() {
// Disconnect without checking the state.
ESP_LOGI(TAG, "[%d] [%s] Disconnecting (conn_id: %d).", this->connection_index_, this->address_str_, this->conn_id_);
if (this->state_ == espbt::ClientState::DISCONNECTING) {
if (this->state() == espbt::ClientState::DISCONNECTING) {
this->log_error_("Already disconnecting");
return;
}
@@ -170,7 +170,7 @@ void BLEClientBase::unconditional_disconnect() {
this->log_gattc_warning_("esp_ble_gattc_close", err);
}
if (this->state_ == espbt::ClientState::DISCOVERED) {
if (this->state() == espbt::ClientState::DISCOVERED) {
this->set_address(0);
this->set_state(espbt::ClientState::IDLE);
} else {
@@ -295,18 +295,18 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
// ESP-IDF's BLE stack may send ESP_GATTC_OPEN_EVT after esp_ble_gattc_open() returns an
// error, if the error occurred at the BTA/GATT layer. This can result in the event
// arriving after we've already transitioned to IDLE state.
if (this->state_ == espbt::ClientState::IDLE) {
if (this->state() == espbt::ClientState::IDLE) {
ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in IDLE state (status=%d), ignoring", this->connection_index_,
this->address_str_, param->open.status);
break;
}
if (this->state_ != espbt::ClientState::CONNECTING) {
if (this->state() != espbt::ClientState::CONNECTING) {
// This should not happen but lets log it in case it does
// because it means we have a bad assumption about how the
// ESP BT stack works.
ESP_LOGE(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in %s state (status=%d)", this->connection_index_,
this->address_str_, espbt::client_state_to_string(this->state_), param->open.status);
this->address_str_, espbt::client_state_to_string(this->state()), param->open.status);
}
if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) {
this->log_gattc_warning_("Connection open", param->open.status);
@@ -327,7 +327,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
// Cached connections already connected with medium parameters, no update needed
// only set our state, subclients might have more stuff to do yet.
this->state_ = espbt::ClientState::ESTABLISHED;
this->set_state_internal_(espbt::ClientState::ESTABLISHED);
break;
}
// For V3_WITHOUT_CACHE, we already set fast params before connecting
@@ -356,7 +356,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
return false;
// Check if we were disconnected while waiting for service discovery
if (param->disconnect.reason == ESP_GATT_CONN_TERMINATE_PEER_USER &&
this->state_ == espbt::ClientState::CONNECTED) {
this->state() == espbt::ClientState::CONNECTED) {
this->log_warning_("Remote closed during discovery");
} else {
ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason 0x%02x", this->connection_index_, this->address_str_,
@@ -433,7 +433,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
#endif
}
ESP_LOGI(TAG, "[%d] [%s] Service discovery complete", this->connection_index_, this->address_str_);
this->state_ = espbt::ClientState::ESTABLISHED;
this->set_state_internal_(espbt::ClientState::ESTABLISHED);
break;
}
case ESP_GATTC_READ_DESCR_EVT: {

View File

@@ -44,7 +44,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
void unconditional_disconnect();
void release_services();
bool connected() { return this->state_ == espbt::ClientState::ESTABLISHED; }
bool connected() { return this->state() == espbt::ClientState::ESTABLISHED; }
void set_auto_connect(bool auto_connect) { this->auto_connect_ = auto_connect; }

View File

@@ -105,15 +105,13 @@ void ESP32BLETracker::loop() {
}
// Check for scan timeout - moved here from scheduler to avoid false reboots
// when the loop is blocked
// when the loop is blocked. This must run every iteration for safety.
if (this->scanner_state_ == ScannerState::RUNNING) {
switch (this->scan_timeout_state_) {
case ScanTimeoutState::MONITORING: {
uint32_t now = App.get_loop_component_start_time();
uint32_t timeout_ms = this->scan_duration_ * 2000;
// Robust time comparison that handles rollover correctly
// This works because unsigned arithmetic wraps around predictably
if ((now - this->scan_start_time_) > timeout_ms) {
if ((App.get_loop_component_start_time() - this->scan_start_time_) > this->scan_timeout_ms_) {
// First time we've seen the timeout exceeded - wait one more loop iteration
// This ensures all components have had a chance to process pending events
// This is because esp32_ble may not have run yet and called
@@ -128,13 +126,31 @@ void ESP32BLETracker::loop() {
ESP_LOGE(TAG, "Scan never terminated, rebooting");
App.reboot();
break;
case ScanTimeoutState::INACTIVE:
// This case should be unreachable - scanner and timeout states are always synchronized
break;
}
}
// Fast path: skip expensive client state counting and processing
// if no state has changed since last loop iteration.
//
// How state changes ensure we reach the code below:
// - handle_scanner_failure_(): scanner_state_ becomes FAILED via set_scanner_state_(), or
// scan_set_param_failed_ requires scanner_state_==RUNNING which can only be reached via
// set_scanner_state_(RUNNING) in gap_scan_start_complete_() (scan params are set during
// STARTING, not RUNNING, so version is always incremented before this condition is true)
// - start_scan_(): scanner_state_ becomes IDLE via set_scanner_state_() in cleanup_scan_state_()
// - try_promote_discovered_clients_(): client enters DISCOVERED via set_state(), or
// connecting client finishes (state change), or scanner reaches RUNNING/IDLE
//
// All conditions that affect the logic below are tied to state changes that increment
// state_version_, so the fast path is safe.
if (this->state_version_ == this->last_processed_version_) {
return;
}
this->last_processed_version_ = this->state_version_;
// State changed - do full processing
ClientStateCounts counts = this->count_client_states_();
if (counts != this->client_state_counts_) {
this->client_state_counts_ = counts;
@@ -142,6 +158,7 @@ void ESP32BLETracker::loop() {
this->client_state_counts_.discovered, this->client_state_counts_.disconnecting);
}
// Scanner failure: reached when set_scanner_state_(FAILED) or scan_set_param_failed_ set
if (this->scanner_state_ == ScannerState::FAILED ||
(this->scan_set_param_failed_ && this->scanner_state_ == ScannerState::RUNNING)) {
this->handle_scanner_failure_();
@@ -160,6 +177,8 @@ void ESP32BLETracker::loop() {
*/
// Start scan: reached when scanner_state_ becomes IDLE (via set_scanner_state_()) and
// all clients are idle (their state changes increment version when they finish)
if (this->scanner_state_ == ScannerState::IDLE && !counts.connecting && !counts.disconnecting && !counts.discovered) {
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
this->update_coex_preference_(false);
@@ -168,8 +187,9 @@ void ESP32BLETracker::loop() {
this->start_scan_(false); // first = false
}
}
// If there is a discovered client and no connecting
// clients, then promote the discovered client to ready to connect.
// Promote discovered clients: reached when a client's state becomes DISCOVERED (via set_state()),
// or when a blocking condition clears (connecting client finishes, scanner reaches RUNNING/IDLE).
// All these trigger state_version_ increment, so we'll process and check promotion eligibility.
// We check both RUNNING and IDLE states because:
// - RUNNING: gap_scan_event_handler initiates stop_scan_() but promotion can happen immediately
// - IDLE: Scanner has already stopped (naturally or by gap_scan_event_handler)
@@ -236,6 +256,7 @@ void ESP32BLETracker::start_scan_(bool first) {
// Start timeout monitoring in loop() instead of using scheduler
// This prevents false reboots when the loop is blocked
this->scan_start_time_ = App.get_loop_component_start_time();
this->scan_timeout_ms_ = this->scan_duration_ * 2000;
this->scan_timeout_state_ = ScanTimeoutState::MONITORING;
esp_err_t err = esp_ble_gap_set_scan_params(&this->scan_params_);
@@ -253,6 +274,10 @@ void ESP32BLETracker::start_scan_(bool first) {
void ESP32BLETracker::register_client(ESPBTClient *client) {
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
client->app_id = ++this->app_id_;
// Give client a pointer to our state_version_ so it can notify us of state changes.
// This enables loop() fast-path optimization - we skip expensive work when no state changed.
// Safe because ESP32BLETracker (singleton) outlives all registered clients.
client->set_tracker_state_version(&this->state_version_);
this->clients_.push_back(client);
this->recalculate_advertisement_parser_types();
#endif
@@ -382,6 +407,7 @@ void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_i
void ESP32BLETracker::set_scanner_state_(ScannerState state) {
this->scanner_state_ = state;
this->state_version_++;
for (auto *listener : this->scanner_state_listeners_) {
listener->on_scanner_state(state);
}

View File

@@ -216,6 +216,19 @@ enum class ConnectionType : uint8_t {
V3_WITHOUT_CACHE
};
/// Base class for BLE GATT clients that connect to remote devices.
///
/// State Change Tracking Design:
/// -----------------------------
/// ESP32BLETracker::loop() needs to know when client states change to avoid
/// expensive polling. Rather than checking all clients every iteration (~7000/min),
/// we use a version counter owned by ESP32BLETracker that clients increment on
/// state changes. The tracker compares versions to skip work when nothing changed.
///
/// Ownership: ESP32BLETracker owns state_version_. Clients hold a non-owning
/// pointer (tracker_state_version_) set during register_client(). Clients
/// increment the counter through this pointer when their state changes.
/// The pointer may be null if the client is not registered with a tracker.
class ESPBTClient : public ESPBTDeviceListener {
public:
virtual bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
@@ -225,26 +238,49 @@ class ESPBTClient : public ESPBTDeviceListener {
virtual void disconnect() = 0;
bool disconnect_pending() const { return this->want_disconnect_; }
void cancel_pending_disconnect() { this->want_disconnect_ = false; }
/// Set the client state with IDLE handling (clears want_disconnect_).
/// Notifies the tracker of state change for loop optimization.
virtual void set_state(ClientState st) {
this->state_ = st;
this->set_state_internal_(st);
if (st == ClientState::IDLE) {
this->want_disconnect_ = false;
}
}
ClientState state() const { return state_; }
ClientState state() const { return this->state_; }
/// Called by ESP32BLETracker::register_client() to enable state change notifications.
/// The pointer must remain valid for the lifetime of the client (guaranteed since
/// ESP32BLETracker is a singleton that outlives all clients).
void set_tracker_state_version(uint8_t *version) { this->tracker_state_version_ = version; }
// Memory optimized layout
uint8_t app_id; // App IDs are small integers assigned sequentially
protected:
// Group 1: 1-byte types
ClientState state_{ClientState::INIT};
/// Set state without IDLE handling - use for direct state transitions.
/// Increments the tracker's state version counter to signal that loop()
/// should do full processing on the next iteration.
void set_state_internal_(ClientState st) {
this->state_ = st;
// Notify tracker that state changed (tracker_state_version_ is owned by ESP32BLETracker)
if (this->tracker_state_version_ != nullptr) {
(*this->tracker_state_version_)++;
}
}
// want_disconnect_ is set to true when a disconnect is requested
// while the client is connecting. This is used to disconnect the
// client as soon as we get the connection id (conn_id_) from the
// ESP_GATTC_OPEN_EVT event.
bool want_disconnect_{false};
// 2 bytes used, 2 bytes padding
private:
ClientState state_{ClientState::INIT};
/// Non-owning pointer to ESP32BLETracker::state_version_. When this client's
/// state changes, we increment the tracker's counter to signal that loop()
/// should perform full processing. Null if client not registered with tracker.
uint8_t *tracker_state_version_{nullptr};
};
class ESP32BLETracker : public Component,
@@ -380,6 +416,16 @@ class ESP32BLETracker : public Component,
// Group 4: 1-byte types (enums, uint8_t, bool)
uint8_t app_id_{0};
uint8_t scan_start_fail_count_{0};
/// Version counter for loop() fast-path optimization. Incremented when:
/// - Scanner state changes (via set_scanner_state_())
/// - Any registered client's state changes (clients hold pointer to this counter)
/// Owned by this class; clients receive non-owning pointer via register_client().
/// When loop() sees state_version_ == last_processed_version_, it skips expensive
/// client state counting and takes the fast path (just timeout check + return).
uint8_t state_version_{0};
/// Last state_version_ value when loop() did full processing. Compared against
/// state_version_ to detect if any state changed since last iteration.
uint8_t last_processed_version_{0};
ScannerState scanner_state_{ScannerState::IDLE};
bool scan_continuous_;
bool scan_active_;
@@ -396,6 +442,8 @@ class ESP32BLETracker : public Component,
EXCEEDED_WAIT, // Timeout exceeded, waiting one loop before reboot
};
uint32_t scan_start_time_{0};
/// Precomputed timeout value: scan_duration_ * 2000
uint32_t scan_timeout_ms_{0};
ScanTimeoutState scan_timeout_state_{ScanTimeoutState::INACTIVE};
};

View File

@@ -69,7 +69,10 @@ void Esp32HostedUpdate::setup() {
// Get coprocessor version
esp_hosted_coprocessor_fwver_t ver_info;
if (esp_hosted_get_coprocessor_fwversion(&ver_info) == ESP_OK) {
this->update_info_.current_version = str_sprintf("%d.%d.%d", ver_info.major1, ver_info.minor1, ver_info.patch1);
// 16 bytes: "255.255.255" (11 chars) + null + safety margin
char buf[16];
snprintf(buf, sizeof(buf), "%d.%d.%d", ver_info.major1, ver_info.minor1, ver_info.patch1);
this->update_info_.current_version = buf;
} else {
this->update_info_.current_version = "unknown";
}

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

@@ -99,7 +99,7 @@ void ESP8266GPIOPin::pin_mode(gpio::Flags flags) {
}
size_t ESP8266GPIOPin::dump_summary(char *buffer, size_t len) const {
return snprintf(buffer, len, "GPIO%u", this->pin_);
return buf_append_printf(buffer, len, 0, "GPIO%u", this->pin_);
}
bool ESP8266GPIOPin::digital_read() {

View File

@@ -90,9 +90,7 @@ async def setup_event_core_(var, config, *, event_types: list[str]):
for conf in config.get(CONF_ON_EVENT, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger, [(cg.std_string, "event_type")], conf
)
await automation.build_automation(trigger, [(cg.StringRef, "event_type")], conf)
cg.add(var.set_event_types(event_types))

View File

@@ -14,10 +14,10 @@ template<typename... Ts> class TriggerEventAction : public Action<Ts...>, public
void play(const Ts &...x) override { this->parent_->trigger(this->event_type_.value(x...)); }
};
class EventTrigger : public Trigger<std::string> {
class EventTrigger : public Trigger<StringRef> {
public:
EventTrigger(Event *event) {
event->add_on_event_callback([this](const std::string &event_type) { this->trigger(event_type); });
event->add_on_event_callback([this](StringRef event_type) { this->trigger(event_type); });
}
};

View File

@@ -23,7 +23,7 @@ void Event::trigger(const std::string &event_type) {
}
this->last_event_type_ = found;
ESP_LOGD(TAG, "'%s' >> '%s'", this->get_name().c_str(), this->last_event_type_);
this->event_callback_.call(event_type);
this->event_callback_.call(StringRef(found));
#if defined(USE_EVENT) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_event(this);
#endif
@@ -45,7 +45,7 @@ void Event::set_event_types(const std::vector<const char *> &event_types) {
this->last_event_type_ = nullptr; // Reset when types change
}
void Event::add_on_event_callback(std::function<void(const std::string &event_type)> &&callback) {
void Event::add_on_event_callback(std::function<void(StringRef event_type)> &&callback) {
this->event_callback_.add(std::move(callback));
}

View File

@@ -70,10 +70,10 @@ class Event : public EntityBase, public EntityBase_DeviceClass {
/// Check if an event has been triggered.
bool has_event() const { return this->last_event_type_ != nullptr; }
void add_on_event_callback(std::function<void(const std::string &event_type)> &&callback);
void add_on_event_callback(std::function<void(StringRef event_type)> &&callback);
protected:
LazyCallbackManager<void(const std::string &event_type)> event_callback_;
LazyCallbackManager<void(StringRef event_type)> event_callback_;
FixedVector<const char *> types_;
private:

View File

@@ -160,7 +160,7 @@ void EZOSensor::loop() {
this->commands_.pop_front();
}
void EZOSensor::add_command_(const std::string &command, EzoCommandType command_type, uint16_t delay_ms) {
void EZOSensor::add_command_(const char *command, EzoCommandType command_type, uint16_t delay_ms) {
std::unique_ptr<EzoCommand> ezo_command(new EzoCommand);
ezo_command->command = command;
ezo_command->command_type = command_type;
@@ -169,13 +169,17 @@ void EZOSensor::add_command_(const std::string &command, EzoCommandType command_
}
void EZOSensor::set_calibration_point_(EzoCalibrationType type, float value) {
std::string payload = str_sprintf("Cal,%s,%0.2f", EZO_CALIBRATION_TYPE_STRINGS[type], value);
// max 21: "Cal,"(4) + type(4) + ","(1) + float(11) + null; use 24 for safety
char payload[24];
snprintf(payload, sizeof(payload), "Cal,%s,%0.2f", EZO_CALIBRATION_TYPE_STRINGS[type], value);
this->add_command_(payload, EzoCommandType::EZO_CALIBRATION, 900);
}
void EZOSensor::set_address(uint8_t address) {
if (address > 0 && address < 128) {
std::string payload = str_sprintf("I2C,%u", address);
// max 8: "I2C,"(4) + uint8(3) + null
char payload[8];
snprintf(payload, sizeof(payload), "I2C,%u", address);
this->new_address_ = address;
this->add_command_(payload, EzoCommandType::EZO_I2C);
} else {
@@ -194,7 +198,9 @@ void EZOSensor::get_slope() { this->add_command_("Slope,?", EzoCommandType::EZO_
void EZOSensor::get_t() { this->add_command_("T,?", EzoCommandType::EZO_T); }
void EZOSensor::set_t(float value) {
std::string payload = str_sprintf("T,%0.2f", value);
// max 14 bytes: "T,"(2) + float with "%0.2f" (up to 11 chars) + null(1); use 16 for alignment
char payload[16];
snprintf(payload, sizeof(payload), "T,%0.2f", value);
this->add_command_(payload, EzoCommandType::EZO_T);
}
@@ -215,7 +221,9 @@ void EZOSensor::set_calibration_point_high(float value) {
}
void EZOSensor::set_calibration_generic(float value) {
std::string payload = str_sprintf("Cal,%0.2f", value);
// exact 16 bytes: "Cal," (4) + float with "%0.2f" (up to 11 chars, e.g. "-9999999.99") + null (1) = 16
char payload[16];
snprintf(payload, sizeof(payload), "Cal,%0.2f", value);
this->add_command_(payload, EzoCommandType::EZO_CALIBRATION, 900);
}
@@ -223,13 +231,11 @@ void EZOSensor::clear_calibration() { this->add_command_("Cal,clear", EzoCommand
void EZOSensor::get_led_state() { this->add_command_("L,?", EzoCommandType::EZO_LED); }
void EZOSensor::set_led_state(bool on) {
std::string to_send = "L,";
to_send += on ? "1" : "0";
this->add_command_(to_send, EzoCommandType::EZO_LED);
}
void EZOSensor::set_led_state(bool on) { this->add_command_(on ? "L,1" : "L,0", EzoCommandType::EZO_LED); }
void EZOSensor::send_custom(const std::string &to_send) { this->add_command_(to_send, EzoCommandType::EZO_CUSTOM); }
void EZOSensor::send_custom(const std::string &to_send) {
this->add_command_(to_send.c_str(), EzoCommandType::EZO_CUSTOM);
}
} // namespace ezo
} // namespace esphome

View File

@@ -92,7 +92,7 @@ class EZOSensor : public sensor::Sensor, public PollingComponent, public i2c::I2
std::deque<std::unique_ptr<EzoCommand>> commands_;
int new_address_;
void add_command_(const std::string &command, EzoCommandType command_type, uint16_t delay_ms = 300);
void add_command_(const char *command, EzoCommandType command_type, uint16_t delay_ms = 300);
void set_calibration_point_(EzoCalibrationType type, float value);

View File

@@ -318,90 +318,93 @@ void EzoPMP::send_next_command_() {
switch (this->next_command_) {
// Read Commands
case EZO_PMP_COMMAND_READ_DOSING: // Page 54
command_buffer_length = sprintf((char *) command_buffer, "D,?");
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "D,?");
break;
case EZO_PMP_COMMAND_READ_SINGLE_REPORT: // Single Report (page 53)
command_buffer_length = sprintf((char *) command_buffer, "R");
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "R");
break;
case EZO_PMP_COMMAND_READ_MAX_FLOW_RATE:
command_buffer_length = sprintf((char *) command_buffer, "DC,?");
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "DC,?");
break;
case EZO_PMP_COMMAND_READ_PAUSE_STATUS:
command_buffer_length = sprintf((char *) command_buffer, "P,?");
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "P,?");
break;
case EZO_PMP_COMMAND_READ_TOTAL_VOLUME_DOSED:
command_buffer_length = sprintf((char *) command_buffer, "TV,?");
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "TV,?");
break;
case EZO_PMP_COMMAND_READ_ABSOLUTE_TOTAL_VOLUME_DOSED:
command_buffer_length = sprintf((char *) command_buffer, "ATV,?");
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "ATV,?");
break;
case EZO_PMP_COMMAND_READ_CALIBRATION_STATUS:
command_buffer_length = sprintf((char *) command_buffer, "Cal,?");
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "Cal,?");
break;
case EZO_PMP_COMMAND_READ_PUMP_VOLTAGE:
command_buffer_length = sprintf((char *) command_buffer, "PV,?");
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "PV,?");
break;
// Non-Read Commands
case EZO_PMP_COMMAND_FIND: // Find (page 52)
command_buffer_length = sprintf((char *) command_buffer, "Find");
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "Find");
wait_time_for_command = 60000; // This command will block all updates for a minute
break;
case EZO_PMP_COMMAND_DOSE_CONTINUOUSLY: // Continuous Dispensing (page 54)
command_buffer_length = sprintf((char *) command_buffer, "D,*");
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "D,*");
break;
case EZO_PMP_COMMAND_CLEAR_TOTAL_VOLUME_DOSED: // Clear Total Volume Dosed (page 64)
command_buffer_length = sprintf((char *) command_buffer, "Clear");
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "Clear");
break;
case EZO_PMP_COMMAND_CLEAR_CALIBRATION: // Clear Calibration (page 65)
command_buffer_length = sprintf((char *) command_buffer, "Cal,clear");
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "Cal,clear");
break;
case EZO_PMP_COMMAND_PAUSE_DOSING: // Pause (page 61)
command_buffer_length = sprintf((char *) command_buffer, "P");
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "P");
break;
case EZO_PMP_COMMAND_STOP_DOSING: // Stop (page 62)
command_buffer_length = sprintf((char *) command_buffer, "X");
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "X");
break;
// Non-Read commands with parameters
case EZO_PMP_COMMAND_DOSE_VOLUME: // Volume Dispensing (page 55)
command_buffer_length = sprintf((char *) command_buffer, "D,%0.1f", this->next_command_volume_);
command_buffer_length =
snprintf((char *) command_buffer, sizeof(command_buffer), "D,%0.1f", this->next_command_volume_);
break;
case EZO_PMP_COMMAND_DOSE_VOLUME_OVER_TIME: // Dose over time (page 56)
command_buffer_length =
sprintf((char *) command_buffer, "D,%0.1f,%i", this->next_command_volume_, this->next_command_duration_);
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "D,%0.1f,%i",
this->next_command_volume_, this->next_command_duration_);
break;
case EZO_PMP_COMMAND_DOSE_WITH_CONSTANT_FLOW_RATE: // Constant Flow Rate (page 57)
command_buffer_length =
sprintf((char *) command_buffer, "DC,%0.1f,%i", this->next_command_volume_, this->next_command_duration_);
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "DC,%0.1f,%i",
this->next_command_volume_, this->next_command_duration_);
break;
case EZO_PMP_COMMAND_SET_CALIBRATION_VOLUME: // Set Calibration Volume (page 65)
command_buffer_length = sprintf((char *) command_buffer, "Cal,%0.2f", this->next_command_volume_);
command_buffer_length =
snprintf((char *) command_buffer, sizeof(command_buffer), "Cal,%0.2f", this->next_command_volume_);
break;
case EZO_PMP_COMMAND_CHANGE_I2C_ADDRESS: // Change I2C Address (page 73)
command_buffer_length = sprintf((char *) command_buffer, "I2C,%i", this->next_command_duration_);
command_buffer_length =
snprintf((char *) command_buffer, sizeof(command_buffer), "I2C,%i", this->next_command_duration_);
break;
case EZO_PMP_COMMAND_EXEC_ARBITRARY_COMMAND_ADDRESS: // Run an arbitrary command
command_buffer_length = sprintf((char *) 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

@@ -77,7 +77,7 @@ FanSpeedSetTrigger = fan_ns.class_(
"FanSpeedSetTrigger", automation.Trigger.template(cg.int_)
)
FanPresetSetTrigger = fan_ns.class_(
"FanPresetSetTrigger", automation.Trigger.template(cg.std_string)
"FanPresetSetTrigger", automation.Trigger.template(cg.StringRef)
)
FanIsOnCondition = fan_ns.class_("FanIsOnCondition", automation.Condition.template())
@@ -287,7 +287,7 @@ async def setup_fan_core_(var, config):
await automation.build_automation(trigger, [(cg.int_, "x")], conf)
for conf in config.get(CONF_ON_PRESET_SET, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(cg.std_string, "x")], conf)
await automation.build_automation(trigger, [(cg.StringRef, "x")], conf)
async def register_fan(var, config):

View File

@@ -208,7 +208,7 @@ class FanSpeedSetTrigger : public Trigger<int> {
int last_speed_;
};
class FanPresetSetTrigger : public Trigger<std::string> {
class FanPresetSetTrigger : public Trigger<StringRef> {
public:
FanPresetSetTrigger(Fan *state) {
state->add_on_state_callback([this, state]() {
@@ -216,7 +216,7 @@ class FanPresetSetTrigger : public Trigger<std::string> {
auto should_trigger = preset_mode != this->last_preset_mode_;
this->last_preset_mode_ = preset_mode;
if (should_trigger) {
this->trigger(std::string(preset_mode));
this->trigger(preset_mode);
}
});
this->last_preset_mode_ = state->get_preset_mode();

View File

@@ -163,9 +163,10 @@ bool GDK101Component::read_fw_version_(uint8_t *data) {
return false;
}
const std::string fw_version_str = str_sprintf("%d.%d", data[0], data[1]);
this->fw_version_text_sensor_->publish_state(fw_version_str);
// max 8: "255.255" (7 chars) + null
char buf[8];
snprintf(buf, sizeof(buf), "%d.%d", data[0], data[1]);
this->fw_version_text_sensor_->publish_state(buf);
}
#endif // USE_TEXT_SENSOR
return true;

View File

@@ -97,7 +97,7 @@ void HomeassistantNumber::control(float value) {
entity_value.key = VALUE_KEY;
// Stack buffer - no heap allocation; %g produces shortest representation
char value_buf[16];
snprintf(value_buf, sizeof(value_buf), "%g", value);
buf_append_printf(value_buf, sizeof(value_buf), 0, "%g", value);
entity_value.value = StringRef(value_buf);
api::global_api_server->send_homeassistant_action(resp);

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

@@ -1,6 +1,6 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.core import CoroPriority, coroutine_with_priority
from esphome.core import CORE, CoroPriority, coroutine_with_priority
CODEOWNERS = ["@esphome/core"]
json_ns = cg.esphome_ns.namespace("json")
@@ -12,6 +12,11 @@ CONFIG_SCHEMA = cv.All(
@coroutine_with_priority(CoroPriority.BUS)
async def to_code(config):
cg.add_library("bblanchon/ArduinoJson", "7.4.2")
if CORE.is_esp32:
from esphome.components.esp32 import add_idf_component
add_idf_component(name="bblanchon/arduinojson", ref="7.4.2")
else:
cg.add_library("bblanchon/ArduinoJson", "7.4.2")
cg.add_define("USE_JSON")
cg.add_global(json_ns.using)

View File

@@ -382,4 +382,11 @@ async def component_to_code(config):
"custom_options.sys_config#h", _BK7231N_SYS_CONFIG_OPTIONS
)
# Disable LWIP statistics to save RAM - not needed in production
# Must explicitly disable all sub-stats to avoid redefinition warnings
cg.add_platformio_option(
"custom_options.lwip",
["LWIP_STATS=0", "MEM_STATS=0", "MEMP_STATS=0"],
)
await cg.register_component(var, config)

View File

@@ -166,8 +166,8 @@ class LibreTinyPreferences : public ESPPreferences {
return true;
}
// Allocate buffer on heap to avoid stack allocation for large data
auto stored_data = std::make_unique<uint8_t[]>(kv.value_len);
// Most preferences are small, use stack buffer with heap fallback for large ones
SmallBufferWithHeapFallback<256> stored_data(kv.value_len);
fdb_blob_make(&this->blob, stored_data.get(), kv.value_len);
size_t actual_len = fdb_kv_get_blob(db, key_str, &this->blob);
if (actual_len != kv.value_len) {

View File

@@ -1,3 +1,4 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#ifdef USE_ESP8266
@@ -44,13 +45,16 @@ void LightWaveRF::send_rx(const std::vector<uint8_t> &msg, uint8_t repeats, bool
}
void LightWaveRF::print_msg_(uint8_t *msg, uint8_t len) {
char buffer[65];
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG
char buffer[65]; // max 10 entries * 6 chars + null
ESP_LOGD(TAG, " Received code (len:%i): ", len);
size_t pos = 0;
for (int i = 0; i < len; i++) {
sprintf(&buffer[i * 6], "0x%02x, ", msg[i]);
pos = buf_append_printf(buffer, sizeof(buffer), pos, "0x%02x, ", msg[i]);
}
ESP_LOGD(TAG, "[%s]", buffer);
#endif
}
void LightWaveRF::dump_config() {

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,8 +1,5 @@
#include "logger.h"
#include <cinttypes>
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
#include <memory> // For unique_ptr
#endif
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
@@ -199,7 +196,8 @@ inline uint8_t Logger::level_for(const char *tag) {
Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate), tx_buffer_size_(tx_buffer_size) {
// add 1 to buffer size for null terminator
this->tx_buffer_ = new char[this->tx_buffer_size_ + 1]; // NOLINT
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed
this->tx_buffer_ = new char[this->tx_buffer_size_ + 1];
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
this->main_task_ = xTaskGetCurrentTaskHandle();
#elif defined(USE_ZEPHYR)
@@ -212,11 +210,14 @@ Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate
void Logger::init_log_buffer(size_t total_buffer_size) {
#ifdef USE_HOST
// Host uses slot count instead of byte size
this->log_buffer_ = esphome::make_unique<logger::TaskLogBufferHost>(total_buffer_size);
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed
this->log_buffer_ = new logger::TaskLogBufferHost(total_buffer_size);
#elif defined(USE_ESP32)
this->log_buffer_ = esphome::make_unique<logger::TaskLogBuffer>(total_buffer_size);
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed
this->log_buffer_ = new logger::TaskLogBuffer(total_buffer_size);
#elif defined(USE_LIBRETINY)
this->log_buffer_ = esphome::make_unique<logger::TaskLogBufferLibreTiny>(total_buffer_size);
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed
this->log_buffer_ = new logger::TaskLogBufferLibreTiny(total_buffer_size);
#endif
#if defined(USE_ESP32) || defined(USE_LIBRETINY)

View File

@@ -412,11 +412,11 @@ class Logger : public Component {
#endif
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
#ifdef USE_HOST
std::unique_ptr<logger::TaskLogBufferHost> log_buffer_; // Will be initialized with init_log_buffer
logger::TaskLogBufferHost *log_buffer_{nullptr}; // Allocated once, never freed
#elif defined(USE_ESP32)
std::unique_ptr<logger::TaskLogBuffer> log_buffer_; // Will be initialized with init_log_buffer
logger::TaskLogBuffer *log_buffer_{nullptr}; // Allocated once, never freed
#elif defined(USE_LIBRETINY)
std::unique_ptr<logger::TaskLogBufferLibreTiny> log_buffer_; // Will be initialized with init_log_buffer
logger::TaskLogBufferLibreTiny *log_buffer_{nullptr}; // Allocated once, never freed
#endif
#endif

View File

@@ -2,6 +2,7 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <cinttypes>
#include <map>
#include <string>
@@ -43,8 +44,17 @@ template<typename K, typename V> class Mapping {
esph_log_e(TAG, "Key '%p' not found in mapping", key);
} else if constexpr (std::is_same_v<K, std::string>) {
esph_log_e(TAG, "Key '%s' not found in mapping", key.c_str());
} else if constexpr (std::is_integral_v<K>) {
char buf[24]; // enough for 64-bit integer
if constexpr (std::is_unsigned_v<K>) {
buf_append_printf(buf, sizeof(buf), 0, "%" PRIu64, static_cast<uint64_t>(key));
} else {
buf_append_printf(buf, sizeof(buf), 0, "%" PRId64, static_cast<int64_t>(key));
}
esph_log_e(TAG, "Key '%s' not found in mapping", buf);
} else {
esph_log_e(TAG, "Key '%s' not found in mapping", to_string(key).c_str());
// All supported key types are handled above - this should never be reached
static_assert(sizeof(K) == 0, "Unsupported key type for Mapping error logging");
}
return {};
}

View File

@@ -162,7 +162,7 @@ void MAX6956GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this-
bool MAX6956GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; }
void MAX6956GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); }
size_t MAX6956GPIOPin::dump_summary(char *buffer, size_t len) const {
return snprintf(buffer, len, "%u via Max6956", this->pin_);
return buf_append_printf(buffer, len, 0, "%u via Max6956", this->pin_);
}
} // namespace max6956

View File

@@ -100,7 +100,7 @@ void MCP23016GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this
bool MCP23016GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; }
void MCP23016GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); }
size_t MCP23016GPIOPin::dump_summary(char *buffer, size_t len) const {
return snprintf(buffer, len, "%u via MCP23016", this->pin_);
return buf_append_printf(buffer, len, 0, "%u via MCP23016", this->pin_);
}
} // namespace mcp23016

View File

@@ -17,7 +17,7 @@ template<uint8_t N> void MCP23XXXGPIOPin<N>::digital_write(bool value) {
this->parent_->digital_write(this->pin_, value != this->inverted_);
}
template<uint8_t N> size_t MCP23XXXGPIOPin<N>::dump_summary(char *buffer, size_t len) const {
return snprintf(buffer, len, "%u via MCP23XXX", this->pin_);
return buf_append_printf(buffer, len, 0, "%u via MCP23XXX", this->pin_);
}
template class MCP23XXXGPIOPin<8>;

View File

@@ -24,13 +24,14 @@ static void register_esp32(MDNSComponent *comp, StaticVector<MDNSService, MDNS_S
mdns_instance_name_set(hostname);
for (const auto &service : services) {
auto txt_records = std::make_unique<mdns_txt_item_t[]>(service.txt_records.size());
// Stack buffer for up to 16 txt records, heap fallback for more
SmallBufferWithHeapFallback<16, mdns_txt_item_t> txt_records(service.txt_records.size());
for (size_t i = 0; i < service.txt_records.size(); i++) {
const auto &record = service.txt_records[i];
// key and value are either compile-time string literals in flash or pointers to dynamic_txt_values_
// Both remain valid for the lifetime of this function, and ESP-IDF makes internal copies
txt_records[i].key = MDNS_STR_ARG(record.key);
txt_records[i].value = MDNS_STR_ARG(record.value);
txt_records.get()[i].key = MDNS_STR_ARG(record.key);
txt_records.get()[i].value = MDNS_STR_ARG(record.value);
}
uint16_t port = const_cast<TemplatableValue<uint16_t> &>(service.port).value();
err = mdns_service_add(nullptr, MDNS_STR_ARG(service.service_type), MDNS_STR_ARG(service.proto), port,

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

@@ -154,7 +154,7 @@ void MPR121GPIOPin::digital_write(bool value) {
}
size_t MPR121GPIOPin::dump_summary(char *buffer, size_t len) const {
return snprintf(buffer, len, "ELE%u on MPR121", this->pin_);
return buf_append_printf(buffer, len, 0, "ELE%u on MPR121", this->pin_);
}
} // namespace mpr121

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(
@@ -396,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;
}
@@ -500,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() {
@@ -610,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
@@ -635,7 +650,8 @@ void MQTTClientComponent::set_log_message_template(MQTTMessage &&message) { this
const MQTTDiscoveryInfo &MQTTClientComponent::get_discovery_info() const { return this->discovery_info_; }
void MQTTClientComponent::set_topic_prefix(const std::string &topic_prefix, const std::string &check_topic_prefix) {
if (App.is_name_add_mac_suffix_enabled() && (topic_prefix == check_topic_prefix)) {
this->topic_prefix_ = str_sanitize(App.get_name());
char buf[ESPHOME_DEVICE_NAME_MAX_LEN + 1];
this->topic_prefix_ = str_sanitize_to(buf, App.get_name().c_str());
} else {
this->topic_prefix_ = topic_prefix;
}

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

@@ -48,7 +48,8 @@ void MQTTComponent::set_subscribe_qos(uint8_t qos) { this->subscribe_qos_ = qos;
void MQTTComponent::set_retain(bool retain) { this->retain_ = retain; }
std::string MQTTComponent::get_discovery_topic_(const MQTTDiscoveryInfo &discovery_info) const {
std::string sanitized_name = str_sanitize(App.get_name());
char sanitized_name[ESPHOME_DEVICE_NAME_MAX_LEN + 1];
str_sanitize_to(sanitized_name, App.get_name().c_str());
const char *comp_type = this->component_type();
char object_id_buf[OBJECT_ID_MAX_LEN];
StringRef object_id = this->get_default_object_id_to_(object_id_buf);
@@ -60,7 +61,7 @@ std::string MQTTComponent::get_discovery_topic_(const MQTTDiscoveryInfo &discove
p = append_char(p, '/');
p = append_str(p, comp_type, strlen(comp_type));
p = append_char(p, '/');
p = append_str(p, sanitized_name.data(), sanitized_name.size());
p = append_str(p, sanitized_name, strlen(sanitized_name));
p = append_char(p, '/');
p = append_str(p, object_id.c_str(), object_id.size());
p = append_str(p, "/config", 7);
@@ -307,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

@@ -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

@@ -1,6 +1,7 @@
#include "nextion.h"
#include <cinttypes>
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/util.h"
@@ -1283,8 +1284,9 @@ void Nextion::check_pending_waveform_() {
size_t buffer_to_send = component->get_wave_buffer_size() < 255 ? component->get_wave_buffer_size()
: 255; // ADDT command can only send 255
std::string command = "addt " + to_string(component->get_component_id()) + "," +
to_string(component->get_wave_channel_id()) + "," + to_string(buffer_to_send);
char command[24]; // "addt " + uint8 + "," + uint8 + "," + uint8 + null = max 17 chars
buf_append_printf(command, sizeof(command), 0, "addt %u,%u,%zu", component->get_component_id(),
component->get_wave_channel_id(), buffer_to_send);
if (!this->send_command_(command)) {
delete nb; // NOLINT(cppcoreguidelines-owning-memory)
this->waveform_queue_.pop_front();

View File

@@ -34,7 +34,7 @@ int Nextion::upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start) {
}
char range_header[32];
sprintf(range_header, "bytes=%" PRIu32 "-%" PRIu32, range_start, range_end);
buf_append_printf(range_header, sizeof(range_header), 0, "bytes=%" PRIu32 "-%" PRIu32, range_start, range_end);
ESP_LOGV(TAG, "Range: %s", range_header);
http_client.addHeader("Range", range_header);
int code = http_client.GET();

View File

@@ -36,7 +36,7 @@ int Nextion::upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &r
}
char range_header[32];
sprintf(range_header, "bytes=%" PRIu32 "-%" PRIu32, range_start, range_end);
buf_append_printf(range_header, sizeof(range_header), 0, "bytes=%" PRIu32 "-%" PRIu32, range_start, range_end);
ESP_LOGV(TAG, "Range: %s", range_header);
esp_http_client_set_header(http_client, "Range", range_header);
ESP_LOGV(TAG, "Open HTTP");

View File

@@ -181,7 +181,7 @@ void PCA6416AGPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this
bool PCA6416AGPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; }
void PCA6416AGPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); }
size_t PCA6416AGPIOPin::dump_summary(char *buffer, size_t len) const {
return snprintf(buffer, len, "%u via PCA6416A", this->pin_);
return buf_append_printf(buffer, len, 0, "%u via PCA6416A", this->pin_);
}
} // namespace pca6416a

View File

@@ -130,7 +130,7 @@ void PCA9554GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this-
bool PCA9554GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; }
void PCA9554GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); }
size_t PCA9554GPIOPin::dump_summary(char *buffer, size_t len) const {
return snprintf(buffer, len, "%u via PCA9554", this->pin_);
return buf_append_printf(buffer, len, 0, "%u via PCA9554", this->pin_);
}
} // namespace pca9554

View File

@@ -107,7 +107,7 @@ void PCF8574GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this-
bool PCF8574GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; }
void PCF8574GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); }
size_t PCF8574GPIOPin::dump_summary(char *buffer, size_t len) const {
return snprintf(buffer, len, "%u via PCF8574", this->pin_);
return buf_append_printf(buffer, len, 0, "%u via PCF8574", this->pin_);
}
} // namespace pcf8574

View File

@@ -165,7 +165,7 @@ void PI4IOE5V6408GPIOPin::digital_write(bool value) {
this->parent_->digital_write(this->pin_, value != this->inverted_);
}
size_t PI4IOE5V6408GPIOPin::dump_summary(char *buffer, size_t len) const {
return snprintf(buffer, len, "%u via PI4IOE5V6408", this->pin_);
return buf_append_printf(buffer, len, 0, "%u via PI4IOE5V6408", this->pin_);
}
} // namespace pi4ioe5v6408

View File

@@ -8,7 +8,7 @@ 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()) {

View File

@@ -1,4 +1,5 @@
#include "rc522_spi.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
// Based on:
@@ -70,7 +71,7 @@ void RC522Spi::pcd_read_register(PcdRegister reg, ///< The register to read fro
index++;
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
sprintf(cstrb, " %x", values[0]);
buf_append_printf(cstrb, sizeof(cstrb), 0, " %x", values[0]);
buf.append(cstrb);
#endif
}
@@ -78,7 +79,7 @@ void RC522Spi::pcd_read_register(PcdRegister reg, ///< The register to read fro
values[index] = transfer_byte(address); // Read value and tell that we want to read the same address again.
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
sprintf(cstrb, " %x", values[index]);
buf_append_printf(cstrb, sizeof(cstrb), 0, " %x", values[index]);
buf.append(cstrb);
#endif
@@ -88,7 +89,7 @@ void RC522Spi::pcd_read_register(PcdRegister reg, ///< The register to read fro
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
buf = buf + " ";
sprintf(cstrb, "%x", values[index]);
buf_append_printf(cstrb, sizeof(cstrb), 0, "%x", values[index]);
buf.append(cstrb);
ESP_LOGVV(TAG, "read_register_array_(%x, %d, , %d) -> %s", reg, count, rx_align, buf.c_str());
@@ -127,7 +128,7 @@ void RC522Spi::pcd_write_register(PcdRegister reg, ///< The register to write t
transfer_byte(values[index]);
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
sprintf(cstrb, " %x", values[index]);
buf_append_printf(cstrb, sizeof(cstrb), 0, " %x", values[index]);
buf.append(cstrb);
#endif
}

View File

@@ -1,6 +1,7 @@
#include "rf_bridge.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <cinttypes>
#include <cstring>
@@ -72,9 +73,9 @@ bool RFBridgeComponent::parse_bridge_byte_(uint8_t byte) {
data.length = raw[2];
data.protocol = raw[3];
char next_byte[3];
char next_byte[3]; // 2 hex chars + null
for (uint8_t i = 0; i < data.length - 1; i++) {
sprintf(next_byte, "%02X", raw[4 + i]);
buf_append_printf(next_byte, sizeof(next_byte), 0, "%02X", raw[4 + i]);
data.code += next_byte;
}
@@ -90,10 +91,10 @@ bool RFBridgeComponent::parse_bridge_byte_(uint8_t byte) {
uint8_t buckets = raw[2] << 1;
std::string str;
char next_byte[3];
char next_byte[3]; // 2 hex chars + null
for (uint32_t i = 0; i <= at; i++) {
sprintf(next_byte, "%02X", raw[i]);
buf_append_printf(next_byte, sizeof(next_byte), 0, "%02X", raw[i]);
str += next_byte;
if ((i > 3) && buckets) {
buckets--;

View File

@@ -33,7 +33,7 @@ SelectPtr = Select.operator("ptr")
# Triggers
SelectStateTrigger = select_ns.class_(
"SelectStateTrigger",
automation.Trigger.template(cg.std_string, cg.size_t),
automation.Trigger.template(cg.StringRef, cg.size_t),
)
# Actions
@@ -100,7 +100,7 @@ async def setup_select_core_(var, config, *, options: list[str]):
for conf in config.get(CONF_ON_VALUE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger, [(cg.std_string, "x"), (cg.size_t, "i")], conf
trigger, [(cg.StringRef, "x"), (cg.size_t, "i")], conf
)
if (mqtt_id := config.get(CONF_MQTT_ID)) is not None:

View File

@@ -6,11 +6,11 @@
namespace esphome::select {
class SelectStateTrigger : public Trigger<std::string, size_t> {
class SelectStateTrigger : public Trigger<StringRef, size_t> {
public:
explicit SelectStateTrigger(Select *parent) : parent_(parent) {
parent->add_on_state_callback(
[this](size_t index) { this->trigger(std::string(this->parent_->option_at(index)), index); });
[this](size_t index) { this->trigger(StringRef(this->parent_->option_at(index)), index); });
}
protected:

View File

@@ -9,6 +9,7 @@ from esphome.const import (
CONF_ABOVE,
CONF_ACCURACY_DECIMALS,
CONF_ALPHA,
CONF_BASELINE,
CONF_BELOW,
CONF_CALIBRATION,
CONF_DEVICE_CLASS,
@@ -38,7 +39,6 @@ from esphome.const import (
CONF_TIMEOUT,
CONF_TO,
CONF_TRIGGER_ID,
CONF_TYPE,
CONF_UNIT_OF_MEASUREMENT,
CONF_VALUE,
CONF_WEB_SERVER,
@@ -107,7 +107,7 @@ from esphome.const import (
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.cpp_generator import MockObjClass
from esphome.cpp_generator import MockObj, MockObjClass
from esphome.util import Registry
CODEOWNERS = ["@esphome/core"]
@@ -574,38 +574,56 @@ async def lambda_filter_to_code(config, filter_id):
return automation.new_lambda_pvariable(filter_id, lambda_, StatelessLambdaFilter)
DELTA_SCHEMA = cv.Schema(
{
cv.Required(CONF_VALUE): cv.positive_float,
cv.Optional(CONF_TYPE, default="absolute"): cv.one_of(
"absolute", "percentage", lower=True
),
}
def validate_delta_value(value):
if isinstance(value, str) and value.endswith("%"):
# Check it's a well-formed percentage, but return the string as-is
try:
cv.positive_float(value[:-1])
return value
except cv.Invalid as exc:
raise cv.Invalid("Malformed delta % value") from exc
return cv.positive_float(value)
# This ideally would be done with `cv.maybe_simple_value` but it doesn't seem to respect the default for min_value.
DELTA_SCHEMA = cv.Any(
cv.All(
{
# Ideally this would be 'default=float("inf")' but it doesn't translate well to C++
cv.Optional(CONF_MAX_VALUE): validate_delta_value,
cv.Optional(CONF_MIN_VALUE, default="0.0"): validate_delta_value,
cv.Optional(CONF_BASELINE): cv.templatable(cv.float_),
},
cv.has_at_least_one_key(CONF_MAX_VALUE, CONF_MIN_VALUE),
),
validate_delta_value,
)
def validate_delta(config):
try:
value = cv.positive_float(config)
return DELTA_SCHEMA({CONF_VALUE: value, CONF_TYPE: "absolute"})
except cv.Invalid:
pass
try:
value = cv.percentage(config)
return DELTA_SCHEMA({CONF_VALUE: value, CONF_TYPE: "percentage"})
except cv.Invalid:
pass
raise cv.Invalid("Delta filter requires a positive number or percentage value.")
def _get_delta(value):
if isinstance(value, str):
assert value.endswith("%")
return 0.0, float(value[:-1])
return value, 0.0
@FILTER_REGISTRY.register("delta", DeltaFilter, cv.Any(DELTA_SCHEMA, validate_delta))
@FILTER_REGISTRY.register("delta", DeltaFilter, DELTA_SCHEMA)
async def delta_filter_to_code(config, filter_id):
percentage = config[CONF_TYPE] == "percentage"
return cg.new_Pvariable(
filter_id,
config[CONF_VALUE],
percentage,
)
# The config could be just the min_value, or it could be a dict.
max = MockObj("std::numeric_limits<float>::infinity()"), 0
if isinstance(config, dict):
min = _get_delta(config[CONF_MIN_VALUE])
if CONF_MAX_VALUE in config:
max = _get_delta(config[CONF_MAX_VALUE])
else:
min = _get_delta(config)
var = cg.new_Pvariable(filter_id, *min, *max)
if isinstance(config, dict) and (baseline_lambda := config.get(CONF_BASELINE)):
baseline = await cg.process_lambda(
baseline_lambda, [(float, "x")], return_type=float
)
cg.add(var.set_baseline(baseline))
return var
@FILTER_REGISTRY.register("or", OrFilter, validate_filters)

View File

@@ -291,22 +291,27 @@ optional<float> ThrottleWithPriorityFilter::new_value(float value) {
}
// DeltaFilter
DeltaFilter::DeltaFilter(float delta, bool percentage_mode)
: delta_(delta), current_delta_(delta), last_value_(NAN), percentage_mode_(percentage_mode) {}
DeltaFilter::DeltaFilter(float min_a0, float min_a1, float max_a0, float max_a1)
: min_a0_(min_a0), min_a1_(min_a1), max_a0_(max_a0), max_a1_(max_a1) {}
void DeltaFilter::set_baseline(float (*fn)(float)) { this->baseline_ = fn; }
optional<float> DeltaFilter::new_value(float value) {
if (std::isnan(value)) {
if (std::isnan(this->last_value_)) {
return {};
} else {
return this->last_value_ = value;
}
// Always yield the first value.
if (std::isnan(this->last_value_)) {
this->last_value_ = value;
return value;
}
float diff = fabsf(value - this->last_value_);
if (std::isnan(this->last_value_) || (diff > 0.0f && diff >= this->current_delta_)) {
if (this->percentage_mode_) {
this->current_delta_ = fabsf(value * this->delta_);
}
return this->last_value_ = value;
// calculate min and max using the linear equation
float ref = this->baseline_(this->last_value_);
float min = fabsf(this->min_a0_ + ref * this->min_a1_);
float max = fabsf(this->max_a0_ + ref * this->max_a1_);
float delta = fabsf(value - ref);
// if there is no reference, e.g. for the first value, just accept this one,
// otherwise accept only if within range.
if (delta > min && delta <= max) {
this->last_value_ = value;
return value;
}
return {};
}

View File

@@ -452,15 +452,21 @@ class HeartbeatFilter : public Filter, public Component {
class DeltaFilter : public Filter {
public:
explicit DeltaFilter(float delta, bool percentage_mode);
explicit DeltaFilter(float min_a0, float min_a1, float max_a0, float max_a1);
void set_baseline(float (*fn)(float));
optional<float> new_value(float value) override;
protected:
float delta_;
float current_delta_;
// These values represent linear equations for the min and max values but in practice only one of a0 and a1 will be
// non-zero Each limit is calculated as fabs(a0 + value * a1)
float min_a0_, min_a1_, max_a0_, max_a1_;
// default baseline is the previous value
float (*baseline_)(float) = [](float last_value) { return last_value; };
float last_value_{NAN};
bool percentage_mode_;
};
class OrFilter : public Filter {

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