mirror of
https://github.com/esphome/esphome.git
synced 2026-01-22 02:49:10 -07:00
Compare commits
89 Commits
globals_po
...
mqtt_stack
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e006216ad3 | ||
|
|
a1c4d56268 | ||
|
|
a9ce3df04c | ||
|
|
99aa83564e | ||
|
|
aa5092bdc2 | ||
|
|
645832a070 | ||
|
|
19c1d3aee7 | ||
|
|
ce5ec7a78f | ||
|
|
ebf589560d | ||
|
|
8dd1aec606 | ||
|
|
d66d05dbfc | ||
|
|
bba447e656 | ||
|
|
9d967b01c8 | ||
|
|
11e0d536e4 | ||
|
|
673f46f761 | ||
|
|
4abae8d445 | ||
|
|
e62368e058 | ||
|
|
5345c96ff3 | ||
|
|
333ace25c9 | ||
|
|
6014bba3d1 | ||
|
|
5f2394ef80 | ||
|
|
29555c0ddc | ||
|
|
37eaf10f75 | ||
|
|
0b60fd0c8c | ||
|
|
fc16ad806a | ||
|
|
7e43abd86f | ||
|
|
7a2734fae9 | ||
|
|
346f3d38d5 | ||
|
|
fbde91358c | ||
|
|
54d6825323 | ||
|
|
307c3e1061 | ||
|
|
df74d307c8 | ||
|
|
acdc7bd892 | ||
|
|
1095bde2db | ||
|
|
258b73d7f6 | ||
|
|
31608543c2 | ||
|
|
41a060668c | ||
|
|
6bad697fc6 | ||
|
|
3ca5e5e4e4 | ||
|
|
cd4cb8b3ec | ||
|
|
1f3a0490a7 | ||
|
|
b08d871add | ||
|
|
15f0986a59 | ||
|
|
90edf32acf | ||
|
|
3c0f43db9e | ||
|
|
6edecd3d45 | ||
|
|
055c00f1ac | ||
|
|
7dc40881e2 | ||
|
|
b04373687e | ||
|
|
b89c127f62 | ||
|
|
47dc5d0a1f | ||
|
|
21886dd3ac | ||
|
|
85a5a26519 | ||
|
|
79ccacd6d6 | ||
|
|
e2319ba651 | ||
|
|
ed4ebffa74 | ||
|
|
c213de4861 | ||
|
|
6cf320fd60 | ||
|
|
aeea340bc6 | ||
|
|
d0e50ed030 | ||
|
|
77b6720a25 | ||
|
|
280d460025 | ||
|
|
ea70faf642 | ||
|
|
5d7b38b261 | ||
|
|
e88093ca60 | ||
|
|
b48d4ab785 | ||
|
|
8ade9dfc10 | ||
|
|
4e0e7796de | ||
|
|
62b6c9bf7c | ||
|
|
b5fe271d6b | ||
|
|
5d787e2512 | ||
|
|
8998ef0bc3 | ||
|
|
8ec31dd769 | ||
|
|
0193464f92 | ||
|
|
1996bc425f | ||
|
|
a0d3d54d69 | ||
|
|
ee264d0fd4 | ||
|
|
892e9b006f | ||
|
|
f8bd4ef57d | ||
|
|
bfcc0e26a3 | ||
|
|
86a1b4cf69 | ||
|
|
2f8f052f43 | ||
|
|
86e70c7e76 | ||
|
|
40025bb277 | ||
|
|
438bb96687 | ||
|
|
c1e1325af2 | ||
|
|
944194e04e | ||
|
|
d27d6d64da | ||
|
|
2182d1e9f0 |
@@ -1 +1 @@
|
||||
d272a88e8ca28ae9340a9a03295a566432a52cb696501908f57764475bf7ca65
|
||||
15dc295268b2dcf75942f42759b3ddec64eba89f75525698eb39c95a7f4b14ce
|
||||
|
||||
2
.github/workflows/sync-device-classes.yml
vendored
2
.github/workflows/sync-device-classes.yml
vendored
@@ -41,7 +41,7 @@ jobs:
|
||||
python script/run-in-env.py pre-commit run --all-files
|
||||
|
||||
- name: Commit changes
|
||||
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
|
||||
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
|
||||
with:
|
||||
commit-message: "Synchronise Device Classes from Home Assistant"
|
||||
committer: esphomebot <esphome@openhomefoundation.org>
|
||||
|
||||
@@ -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
139
esphome/build_gen/espidf.py
Normal 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),
|
||||
)
|
||||
@@ -69,6 +69,7 @@ from esphome.cpp_types import ( # noqa: F401
|
||||
JsonObjectConst,
|
||||
Parented,
|
||||
PollingComponent,
|
||||
StringRef,
|
||||
arduino_json_ns,
|
||||
bool_,
|
||||
const_char_ptr,
|
||||
|
||||
@@ -160,21 +160,21 @@ async def to_code(config):
|
||||
zephyr_add_user("io-channels", f"<&adc {channel_id}>")
|
||||
zephyr_add_overlay(
|
||||
f"""
|
||||
&adc {{
|
||||
#address-cells = <1>;
|
||||
#size-cells = <0>;
|
||||
&adc {{
|
||||
#address-cells = <1>;
|
||||
#size-cells = <0>;
|
||||
|
||||
channel@{channel_id} {{
|
||||
reg = <{channel_id}>;
|
||||
zephyr,gain = "{gain}";
|
||||
zephyr,reference = "ADC_REF_INTERNAL";
|
||||
zephyr,acquisition-time = <ADC_ACQ_TIME_DEFAULT>;
|
||||
zephyr,input-positive = <NRF_SAADC_{pin_number}>;
|
||||
zephyr,resolution = <14>;
|
||||
zephyr,oversampling = <8>;
|
||||
}};
|
||||
}};
|
||||
"""
|
||||
channel@{channel_id} {{
|
||||
reg = <{channel_id}>;
|
||||
zephyr,gain = "{gain}";
|
||||
zephyr,reference = "ADC_REF_INTERNAL";
|
||||
zephyr,acquisition-time = <ADC_ACQ_TIME_DEFAULT>;
|
||||
zephyr,input-positive = <NRF_SAADC_{pin_number}>;
|
||||
zephyr,resolution = <14>;
|
||||
zephyr,oversampling = <8>;
|
||||
}};
|
||||
}};
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -67,52 +67,29 @@ void AlarmControlPanel::add_on_ready_callback(std::function<void()> &&callback)
|
||||
this->ready_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void AlarmControlPanel::arm_away(optional<std::string> code) {
|
||||
void AlarmControlPanel::arm_with_code_(AlarmControlPanelCall &(AlarmControlPanelCall::*arm_method)(),
|
||||
const char *code) {
|
||||
auto call = this->make_call();
|
||||
call.arm_away();
|
||||
if (code.has_value())
|
||||
call.set_code(code.value());
|
||||
(call.*arm_method)();
|
||||
if (code != nullptr)
|
||||
call.set_code(code);
|
||||
call.perform();
|
||||
}
|
||||
|
||||
void AlarmControlPanel::arm_home(optional<std::string> code) {
|
||||
auto call = this->make_call();
|
||||
call.arm_home();
|
||||
if (code.has_value())
|
||||
call.set_code(code.value());
|
||||
call.perform();
|
||||
void AlarmControlPanel::arm_away(const char *code) { this->arm_with_code_(&AlarmControlPanelCall::arm_away, code); }
|
||||
|
||||
void AlarmControlPanel::arm_home(const char *code) { this->arm_with_code_(&AlarmControlPanelCall::arm_home, code); }
|
||||
|
||||
void AlarmControlPanel::arm_night(const char *code) { this->arm_with_code_(&AlarmControlPanelCall::arm_night, code); }
|
||||
|
||||
void AlarmControlPanel::arm_vacation(const char *code) {
|
||||
this->arm_with_code_(&AlarmControlPanelCall::arm_vacation, code);
|
||||
}
|
||||
|
||||
void AlarmControlPanel::arm_night(optional<std::string> code) {
|
||||
auto call = this->make_call();
|
||||
call.arm_night();
|
||||
if (code.has_value())
|
||||
call.set_code(code.value());
|
||||
call.perform();
|
||||
void AlarmControlPanel::arm_custom_bypass(const char *code) {
|
||||
this->arm_with_code_(&AlarmControlPanelCall::arm_custom_bypass, code);
|
||||
}
|
||||
|
||||
void AlarmControlPanel::arm_vacation(optional<std::string> code) {
|
||||
auto call = this->make_call();
|
||||
call.arm_vacation();
|
||||
if (code.has_value())
|
||||
call.set_code(code.value());
|
||||
call.perform();
|
||||
}
|
||||
|
||||
void AlarmControlPanel::arm_custom_bypass(optional<std::string> code) {
|
||||
auto call = this->make_call();
|
||||
call.arm_custom_bypass();
|
||||
if (code.has_value())
|
||||
call.set_code(code.value());
|
||||
call.perform();
|
||||
}
|
||||
|
||||
void AlarmControlPanel::disarm(optional<std::string> code) {
|
||||
auto call = this->make_call();
|
||||
call.disarm();
|
||||
if (code.has_value())
|
||||
call.set_code(code.value());
|
||||
call.perform();
|
||||
}
|
||||
void AlarmControlPanel::disarm(const char *code) { this->arm_with_code_(&AlarmControlPanelCall::disarm, code); }
|
||||
|
||||
} // namespace esphome::alarm_control_panel
|
||||
|
||||
@@ -76,37 +76,53 @@ class AlarmControlPanel : public EntityBase {
|
||||
*
|
||||
* @param code The code
|
||||
*/
|
||||
void arm_away(optional<std::string> code = nullopt);
|
||||
void arm_away(const char *code = nullptr);
|
||||
void arm_away(const optional<std::string> &code) {
|
||||
this->arm_away(code.has_value() ? code.value().c_str() : nullptr);
|
||||
}
|
||||
|
||||
/** arm the alarm in home mode
|
||||
*
|
||||
* @param code The code
|
||||
*/
|
||||
void arm_home(optional<std::string> code = nullopt);
|
||||
void arm_home(const char *code = nullptr);
|
||||
void arm_home(const optional<std::string> &code) {
|
||||
this->arm_home(code.has_value() ? code.value().c_str() : nullptr);
|
||||
}
|
||||
|
||||
/** arm the alarm in night mode
|
||||
*
|
||||
* @param code The code
|
||||
*/
|
||||
void arm_night(optional<std::string> code = nullopt);
|
||||
void arm_night(const char *code = nullptr);
|
||||
void arm_night(const optional<std::string> &code) {
|
||||
this->arm_night(code.has_value() ? code.value().c_str() : nullptr);
|
||||
}
|
||||
|
||||
/** arm the alarm in vacation mode
|
||||
*
|
||||
* @param code The code
|
||||
*/
|
||||
void arm_vacation(optional<std::string> code = nullopt);
|
||||
void arm_vacation(const char *code = nullptr);
|
||||
void arm_vacation(const optional<std::string> &code) {
|
||||
this->arm_vacation(code.has_value() ? code.value().c_str() : nullptr);
|
||||
}
|
||||
|
||||
/** arm the alarm in custom bypass mode
|
||||
*
|
||||
* @param code The code
|
||||
*/
|
||||
void arm_custom_bypass(optional<std::string> code = nullopt);
|
||||
void arm_custom_bypass(const char *code = nullptr);
|
||||
void arm_custom_bypass(const optional<std::string> &code) {
|
||||
this->arm_custom_bypass(code.has_value() ? code.value().c_str() : nullptr);
|
||||
}
|
||||
|
||||
/** disarm the alarm
|
||||
*
|
||||
* @param code The code
|
||||
*/
|
||||
void disarm(optional<std::string> code = nullopt);
|
||||
void disarm(const char *code = nullptr);
|
||||
void disarm(const optional<std::string> &code) { this->disarm(code.has_value() ? code.value().c_str() : nullptr); }
|
||||
|
||||
/** Get the state
|
||||
*
|
||||
@@ -118,6 +134,8 @@ class AlarmControlPanel : public EntityBase {
|
||||
|
||||
protected:
|
||||
friend AlarmControlPanelCall;
|
||||
// Helper to reduce code duplication for arm/disarm methods
|
||||
void arm_with_code_(AlarmControlPanelCall &(AlarmControlPanelCall::*arm_method)(), const char *code);
|
||||
// in order to store last panel state in flash
|
||||
ESPPreferenceObject pref_;
|
||||
// current state
|
||||
|
||||
@@ -10,8 +10,10 @@ static const char *const TAG = "alarm_control_panel";
|
||||
|
||||
AlarmControlPanelCall::AlarmControlPanelCall(AlarmControlPanel *parent) : parent_(parent) {}
|
||||
|
||||
AlarmControlPanelCall &AlarmControlPanelCall::set_code(const std::string &code) {
|
||||
this->code_ = code;
|
||||
AlarmControlPanelCall &AlarmControlPanelCall::set_code(const char *code) {
|
||||
if (code != nullptr) {
|
||||
this->code_ = std::string(code);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,8 @@ class AlarmControlPanelCall {
|
||||
public:
|
||||
AlarmControlPanelCall(AlarmControlPanel *parent);
|
||||
|
||||
AlarmControlPanelCall &set_code(const std::string &code);
|
||||
AlarmControlPanelCall &set_code(const char *code);
|
||||
AlarmControlPanelCall &set_code(const std::string &code) { return this->set_code(code.c_str()); }
|
||||
AlarmControlPanelCall &arm_away();
|
||||
AlarmControlPanelCall &arm_home();
|
||||
AlarmControlPanelCall &arm_night();
|
||||
|
||||
@@ -66,15 +66,7 @@ template<typename... Ts> class ArmAwayAction : public Action<Ts...> {
|
||||
|
||||
TEMPLATABLE_VALUE(std::string, code)
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
auto call = this->alarm_control_panel_->make_call();
|
||||
auto code = this->code_.optional_value(x...);
|
||||
if (code.has_value()) {
|
||||
call.set_code(code.value());
|
||||
}
|
||||
call.arm_away();
|
||||
call.perform();
|
||||
}
|
||||
void play(const Ts &...x) override { this->alarm_control_panel_->arm_away(this->code_.optional_value(x...)); }
|
||||
|
||||
protected:
|
||||
AlarmControlPanel *alarm_control_panel_;
|
||||
@@ -86,15 +78,7 @@ template<typename... Ts> class ArmHomeAction : public Action<Ts...> {
|
||||
|
||||
TEMPLATABLE_VALUE(std::string, code)
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
auto call = this->alarm_control_panel_->make_call();
|
||||
auto code = this->code_.optional_value(x...);
|
||||
if (code.has_value()) {
|
||||
call.set_code(code.value());
|
||||
}
|
||||
call.arm_home();
|
||||
call.perform();
|
||||
}
|
||||
void play(const Ts &...x) override { this->alarm_control_panel_->arm_home(this->code_.optional_value(x...)); }
|
||||
|
||||
protected:
|
||||
AlarmControlPanel *alarm_control_panel_;
|
||||
@@ -106,15 +90,7 @@ template<typename... Ts> class ArmNightAction : public Action<Ts...> {
|
||||
|
||||
TEMPLATABLE_VALUE(std::string, code)
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
auto call = this->alarm_control_panel_->make_call();
|
||||
auto code = this->code_.optional_value(x...);
|
||||
if (code.has_value()) {
|
||||
call.set_code(code.value());
|
||||
}
|
||||
call.arm_night();
|
||||
call.perform();
|
||||
}
|
||||
void play(const Ts &...x) override { this->alarm_control_panel_->arm_night(this->code_.optional_value(x...)); }
|
||||
|
||||
protected:
|
||||
AlarmControlPanel *alarm_control_panel_;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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_();
|
||||
}
|
||||
|
||||
|
||||
@@ -1715,7 +1715,7 @@ void APIConnection::on_home_assistant_state_response(const HomeAssistantStateRes
|
||||
// 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<256> state_buf_alloc(state_len + 1);
|
||||
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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.esp32 import add_idf_component
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_BITS_PER_SAMPLE, CONF_NUM_CHANNELS, CONF_SAMPLE_RATE
|
||||
import esphome.final_validate as fv
|
||||
@@ -165,4 +166,7 @@ def final_validate_audio_schema(
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
cg.add_library("esphome/esp-audio-libs", "2.0.1")
|
||||
add_idf_component(
|
||||
name="esphome/esp-audio-libs",
|
||||
ref="2.0.3",
|
||||
)
|
||||
|
||||
@@ -300,7 +300,7 @@ FileDecoderState AudioDecoder::decode_mp3_() {
|
||||
|
||||
// Advance read pointer to match the offset for the syncword
|
||||
this->input_transfer_buffer_->decrease_buffer_length(offset);
|
||||
uint8_t *buffer_start = this->input_transfer_buffer_->get_buffer_start();
|
||||
const uint8_t *buffer_start = this->input_transfer_buffer_->get_buffer_start();
|
||||
|
||||
buffer_length = (int) this->input_transfer_buffer_->available();
|
||||
int err = esp_audio_libs::helix_decoder::MP3Decode(this->mp3_decoder_, &buffer_start, &buffer_length,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -76,7 +76,6 @@ class CS5460AComponent : public Component,
|
||||
void restart() { restart_(); }
|
||||
|
||||
void setup() override;
|
||||
void loop() override {}
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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_);
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
@@ -132,6 +132,26 @@ void DebugComponent::log_partition_info_() {
|
||||
flash_area_foreach(fa_cb, nullptr);
|
||||
}
|
||||
|
||||
static const char *regout0_to_str(uint32_t value) {
|
||||
switch (value) {
|
||||
case (UICR_REGOUT0_VOUT_DEFAULT):
|
||||
return "1.8V (default)";
|
||||
case (UICR_REGOUT0_VOUT_1V8):
|
||||
return "1.8V";
|
||||
case (UICR_REGOUT0_VOUT_2V1):
|
||||
return "2.1V";
|
||||
case (UICR_REGOUT0_VOUT_2V4):
|
||||
return "2.4V";
|
||||
case (UICR_REGOUT0_VOUT_2V7):
|
||||
return "2.7V";
|
||||
case (UICR_REGOUT0_VOUT_3V0):
|
||||
return "3.0V";
|
||||
case (UICR_REGOUT0_VOUT_3V3):
|
||||
return "3.3V";
|
||||
}
|
||||
return "???V";
|
||||
}
|
||||
|
||||
size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE> buffer, size_t pos) {
|
||||
constexpr size_t size = DEVICE_INFO_BUFFER_SIZE;
|
||||
char *buf = buffer.data();
|
||||
@@ -140,48 +160,28 @@ 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) {
|
||||
const char *reg0_type = nrf_power_dcdcen_vddh_get(NRF_POWER) ? "DC/DC" : "LDO";
|
||||
const char *reg0_voltage;
|
||||
switch (NRF_UICR->REGOUT0 & UICR_REGOUT0_VOUT_Msk) {
|
||||
case (UICR_REGOUT0_VOUT_DEFAULT << UICR_REGOUT0_VOUT_Pos):
|
||||
reg0_voltage = "1.8V (default)";
|
||||
break;
|
||||
case (UICR_REGOUT0_VOUT_1V8 << UICR_REGOUT0_VOUT_Pos):
|
||||
reg0_voltage = "1.8V";
|
||||
break;
|
||||
case (UICR_REGOUT0_VOUT_2V1 << UICR_REGOUT0_VOUT_Pos):
|
||||
reg0_voltage = "2.1V";
|
||||
break;
|
||||
case (UICR_REGOUT0_VOUT_2V4 << UICR_REGOUT0_VOUT_Pos):
|
||||
reg0_voltage = "2.4V";
|
||||
break;
|
||||
case (UICR_REGOUT0_VOUT_2V7 << UICR_REGOUT0_VOUT_Pos):
|
||||
reg0_voltage = "2.7V";
|
||||
break;
|
||||
case (UICR_REGOUT0_VOUT_3V0 << UICR_REGOUT0_VOUT_Pos):
|
||||
reg0_voltage = "3.0V";
|
||||
break;
|
||||
case (UICR_REGOUT0_VOUT_3V3 << UICR_REGOUT0_VOUT_Pos):
|
||||
reg0_voltage = "3.3V";
|
||||
break;
|
||||
default:
|
||||
reg0_voltage = "???V";
|
||||
}
|
||||
const char *reg0_voltage = regout0_to_str((NRF_UICR->REGOUT0 & UICR_REGOUT0_VOUT_Msk) >> UICR_REGOUT0_VOUT_Pos);
|
||||
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);
|
||||
#ifdef USE_NRF52_REG0_VOUT
|
||||
if ((NRF_UICR->REGOUT0 & UICR_REGOUT0_VOUT_Msk) >> UICR_REGOUT0_VOUT_Pos != USE_NRF52_REG0_VOUT) {
|
||||
ESP_LOGE(TAG, "Regulator stage 0: expected %s", regout0_to_str(USE_NRF52_REG0_VOUT));
|
||||
}
|
||||
#endif
|
||||
} 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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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_,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -190,7 +190,7 @@ async def to_code(config):
|
||||
# Rotation is handled by setting the transform
|
||||
display_config = {k: v for k, v in config.items() if k != CONF_ROTATION}
|
||||
await display.register_display(var, display_config)
|
||||
await spi.register_spi_device(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
|
||||
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
|
||||
cg.add(var.set_dc_pin(dc))
|
||||
|
||||
@@ -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,
|
||||
@@ -180,6 +182,12 @@ def set_core_data(config):
|
||||
path=[CONF_CPU_FREQUENCY],
|
||||
)
|
||||
|
||||
if variant == VARIANT_ESP32P4 and cpu_frequency == "400MHZ":
|
||||
_LOGGER.warning(
|
||||
"400MHz on ESP32-P4 is experimental and may not boot. "
|
||||
"Consider using 360MHz instead. See https://github.com/esphome/esphome/issues/13425"
|
||||
)
|
||||
|
||||
CORE.data[KEY_ESP32] = {}
|
||||
CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_ESP32
|
||||
conf = config[CONF_FRAMEWORK]
|
||||
@@ -199,6 +207,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] = {}
|
||||
|
||||
@@ -339,7 +348,12 @@ def add_extra_build_file(filename: str, path: Path) -> bool:
|
||||
def _format_framework_arduino_version(ver: cv.Version) -> str:
|
||||
# format the given arduino (https://github.com/espressif/arduino-esp32/releases) version to
|
||||
# a PIO pioarduino/framework-arduinoespressif32 value
|
||||
return f"pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/{str(ver)}/esp32-{str(ver)}.zip"
|
||||
# 3.3.6+ changed filename from esp32-{ver}.zip to esp32-core-{ver}.tar.xz
|
||||
if ver >= cv.Version(3, 3, 6):
|
||||
filename = f"esp32-core-{ver}.tar.xz"
|
||||
else:
|
||||
filename = f"esp32-{ver}.zip"
|
||||
return f"pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/{ver}/{filename}"
|
||||
|
||||
|
||||
def _format_framework_espidf_version(ver: cv.Version, release: str) -> str:
|
||||
@@ -374,11 +388,12 @@ def _is_framework_url(source: str) -> bool:
|
||||
# The default/recommended arduino framework version
|
||||
# - https://github.com/espressif/arduino-esp32/releases
|
||||
ARDUINO_FRAMEWORK_VERSION_LOOKUP = {
|
||||
"recommended": cv.Version(3, 3, 5),
|
||||
"latest": cv.Version(3, 3, 5),
|
||||
"dev": cv.Version(3, 3, 5),
|
||||
"recommended": cv.Version(3, 3, 6),
|
||||
"latest": cv.Version(3, 3, 6),
|
||||
"dev": cv.Version(3, 3, 6),
|
||||
}
|
||||
ARDUINO_PLATFORM_VERSION_LOOKUP = {
|
||||
cv.Version(3, 3, 6): cv.Version(55, 3, 36),
|
||||
cv.Version(3, 3, 5): cv.Version(55, 3, 35),
|
||||
cv.Version(3, 3, 4): cv.Version(55, 3, 31, "2"),
|
||||
cv.Version(3, 3, 3): cv.Version(55, 3, 31, "2"),
|
||||
@@ -396,6 +411,7 @@ ARDUINO_PLATFORM_VERSION_LOOKUP = {
|
||||
# These versions correspond to pioarduino/esp-idf releases
|
||||
# See: https://github.com/pioarduino/esp-idf/releases
|
||||
ARDUINO_IDF_VERSION_LOOKUP = {
|
||||
cv.Version(3, 3, 6): cv.Version(5, 5, 2),
|
||||
cv.Version(3, 3, 5): cv.Version(5, 5, 2),
|
||||
cv.Version(3, 3, 4): cv.Version(5, 5, 1),
|
||||
cv.Version(3, 3, 3): cv.Version(5, 5, 1),
|
||||
@@ -418,7 +434,7 @@ ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
|
||||
"dev": cv.Version(5, 5, 2),
|
||||
}
|
||||
ESP_IDF_PLATFORM_VERSION_LOOKUP = {
|
||||
cv.Version(5, 5, 2): cv.Version(55, 3, 35),
|
||||
cv.Version(5, 5, 2): cv.Version(55, 3, 36),
|
||||
cv.Version(5, 5, 1): cv.Version(55, 3, 31, "2"),
|
||||
cv.Version(5, 5, 0): cv.Version(55, 3, 31, "2"),
|
||||
cv.Version(5, 4, 3): cv.Version(55, 3, 32),
|
||||
@@ -435,9 +451,9 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = {
|
||||
# The platform-espressif32 version
|
||||
# - https://github.com/pioarduino/platform-espressif32/releases
|
||||
PLATFORM_VERSION_LOOKUP = {
|
||||
"recommended": cv.Version(55, 3, 35),
|
||||
"latest": cv.Version(55, 3, 35),
|
||||
"dev": cv.Version(55, 3, 35),
|
||||
"recommended": cv.Version(55, 3, 36),
|
||||
"latest": cv.Version(55, 3, 36),
|
||||
"dev": cv.Version(55, 3, 36),
|
||||
}
|
||||
|
||||
|
||||
@@ -962,12 +978,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 +1035,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 +1224,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 +1390,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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
};
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ extern "C" {
|
||||
#include "preferences.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
|
||||
namespace esphome::esp8266 {
|
||||
|
||||
@@ -143,16 +142,8 @@ class ESP8266PreferenceBackend : public ESPPreferenceBackend {
|
||||
return false;
|
||||
|
||||
const size_t buffer_size = static_cast<size_t>(this->length_words) + 1;
|
||||
uint32_t stack_buffer[PREF_BUFFER_WORDS];
|
||||
std::unique_ptr<uint32_t[]> heap_buffer;
|
||||
uint32_t *buffer;
|
||||
|
||||
if (buffer_size <= PREF_BUFFER_WORDS) {
|
||||
buffer = stack_buffer;
|
||||
} else {
|
||||
heap_buffer = make_unique<uint32_t[]>(buffer_size);
|
||||
buffer = heap_buffer.get();
|
||||
}
|
||||
SmallBufferWithHeapFallback<PREF_BUFFER_WORDS, uint32_t> buffer_alloc(buffer_size);
|
||||
uint32_t *buffer = buffer_alloc.get();
|
||||
memset(buffer, 0, buffer_size * sizeof(uint32_t));
|
||||
|
||||
memcpy(buffer, data, len);
|
||||
@@ -167,16 +158,8 @@ class ESP8266PreferenceBackend : public ESPPreferenceBackend {
|
||||
return false;
|
||||
|
||||
const size_t buffer_size = static_cast<size_t>(this->length_words) + 1;
|
||||
uint32_t stack_buffer[PREF_BUFFER_WORDS];
|
||||
std::unique_ptr<uint32_t[]> heap_buffer;
|
||||
uint32_t *buffer;
|
||||
|
||||
if (buffer_size <= PREF_BUFFER_WORDS) {
|
||||
buffer = stack_buffer;
|
||||
} else {
|
||||
heap_buffer = make_unique<uint32_t[]>(buffer_size);
|
||||
buffer = heap_buffer.get();
|
||||
}
|
||||
SmallBufferWithHeapFallback<PREF_BUFFER_WORDS, uint32_t> buffer_alloc(buffer_size);
|
||||
uint32_t *buffer = buffer_alloc.get();
|
||||
|
||||
bool ret = this->in_flash ? load_from_flash(this->offset, buffer, buffer_size)
|
||||
: load_from_rtc(this->offset, buffer, buffer_size);
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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); });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -9,55 +9,29 @@ from esphome.const import (
|
||||
CONF_VALUE,
|
||||
)
|
||||
from esphome.core import CoroPriority, coroutine_with_priority
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
globals_ns = cg.esphome_ns.namespace("globals")
|
||||
GlobalsComponent = globals_ns.class_("GlobalsComponent", cg.Component)
|
||||
RestoringGlobalsComponent = globals_ns.class_(
|
||||
"RestoringGlobalsComponent", cg.PollingComponent
|
||||
)
|
||||
RestoringGlobalsComponent = globals_ns.class_("RestoringGlobalsComponent", cg.Component)
|
||||
RestoringGlobalStringComponent = globals_ns.class_(
|
||||
"RestoringGlobalStringComponent", cg.PollingComponent
|
||||
"RestoringGlobalStringComponent", cg.Component
|
||||
)
|
||||
GlobalVarSetAction = globals_ns.class_("GlobalVarSetAction", automation.Action)
|
||||
|
||||
CONF_MAX_RESTORE_DATA_LENGTH = "max_restore_data_length"
|
||||
|
||||
# Base schema fields shared by both variants
|
||||
_BASE_SCHEMA = {
|
||||
cv.Required(CONF_ID): cv.declare_id(GlobalsComponent),
|
||||
cv.Required(CONF_TYPE): cv.string_strict,
|
||||
cv.Optional(CONF_INITIAL_VALUE): cv.string_strict,
|
||||
cv.Optional(CONF_MAX_RESTORE_DATA_LENGTH): cv.int_range(0, 254),
|
||||
}
|
||||
|
||||
# Non-restoring globals: regular Component (no polling needed)
|
||||
_NON_RESTORING_SCHEMA = cv.Schema(
|
||||
{
|
||||
**_BASE_SCHEMA,
|
||||
cv.Optional(CONF_RESTORE_VALUE, default=False): cv.boolean,
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
# Restoring globals: PollingComponent with configurable update_interval
|
||||
_RESTORING_SCHEMA = cv.Schema(
|
||||
{
|
||||
**_BASE_SCHEMA,
|
||||
cv.Optional(CONF_RESTORE_VALUE, default=True): cv.boolean,
|
||||
}
|
||||
).extend(cv.polling_component_schema("1s"))
|
||||
|
||||
|
||||
def _globals_schema(config: ConfigType) -> ConfigType:
|
||||
"""Select schema based on restore_value setting."""
|
||||
if config.get(CONF_RESTORE_VALUE, False):
|
||||
return _RESTORING_SCHEMA(config)
|
||||
return _NON_RESTORING_SCHEMA(config)
|
||||
|
||||
|
||||
MULTI_CONF = True
|
||||
CONFIG_SCHEMA = _globals_schema
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.declare_id(GlobalsComponent),
|
||||
cv.Required(CONF_TYPE): cv.string_strict,
|
||||
cv.Optional(CONF_INITIAL_VALUE): cv.string_strict,
|
||||
cv.Optional(CONF_RESTORE_VALUE, default=False): cv.boolean,
|
||||
cv.Optional(CONF_MAX_RESTORE_DATA_LENGTH): cv.int_range(0, 254),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
# Run with low priority so that namespaces are registered first
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include <cstring>
|
||||
|
||||
namespace esphome::globals {
|
||||
namespace esphome {
|
||||
namespace globals {
|
||||
|
||||
template<typename T> class GlobalsComponent : public Component {
|
||||
public:
|
||||
@@ -23,14 +24,13 @@ template<typename T> class GlobalsComponent : public Component {
|
||||
T value_{};
|
||||
};
|
||||
|
||||
template<typename T> class RestoringGlobalsComponent : public PollingComponent {
|
||||
template<typename T> class RestoringGlobalsComponent : public Component {
|
||||
public:
|
||||
using value_type = T;
|
||||
explicit RestoringGlobalsComponent() : PollingComponent(1000) {}
|
||||
explicit RestoringGlobalsComponent(T initial_value) : PollingComponent(1000), value_(initial_value) {}
|
||||
explicit RestoringGlobalsComponent() = default;
|
||||
explicit RestoringGlobalsComponent(T initial_value) : value_(initial_value) {}
|
||||
explicit RestoringGlobalsComponent(
|
||||
std::array<typename std::remove_extent<T>::type, std::extent<T>::value> initial_value)
|
||||
: PollingComponent(1000) {
|
||||
std::array<typename std::remove_extent<T>::type, std::extent<T>::value> initial_value) {
|
||||
memcpy(this->value_, initial_value.data(), sizeof(T));
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ template<typename T> class RestoringGlobalsComponent : public PollingComponent {
|
||||
|
||||
float get_setup_priority() const override { return setup_priority::HARDWARE; }
|
||||
|
||||
void update() override { store_value_(); }
|
||||
void loop() override { store_value_(); }
|
||||
|
||||
void on_shutdown() override { store_value_(); }
|
||||
|
||||
@@ -66,14 +66,13 @@ template<typename T> class RestoringGlobalsComponent : public PollingComponent {
|
||||
};
|
||||
|
||||
// Use with string or subclasses of strings
|
||||
template<typename T, uint8_t SZ> class RestoringGlobalStringComponent : public PollingComponent {
|
||||
template<typename T, uint8_t SZ> class RestoringGlobalStringComponent : public Component {
|
||||
public:
|
||||
using value_type = T;
|
||||
explicit RestoringGlobalStringComponent() : PollingComponent(1000) {}
|
||||
explicit RestoringGlobalStringComponent(T initial_value) : PollingComponent(1000) { this->value_ = initial_value; }
|
||||
explicit RestoringGlobalStringComponent() = default;
|
||||
explicit RestoringGlobalStringComponent(T initial_value) { this->value_ = initial_value; }
|
||||
explicit RestoringGlobalStringComponent(
|
||||
std::array<typename std::remove_extent<T>::type, std::extent<T>::value> initial_value)
|
||||
: PollingComponent(1000) {
|
||||
std::array<typename std::remove_extent<T>::type, std::extent<T>::value> initial_value) {
|
||||
memcpy(this->value_, initial_value.data(), sizeof(T));
|
||||
}
|
||||
|
||||
@@ -91,7 +90,7 @@ template<typename T, uint8_t SZ> class RestoringGlobalStringComponent : public P
|
||||
|
||||
float get_setup_priority() const override { return setup_priority::HARDWARE; }
|
||||
|
||||
void update() override { store_value_(); }
|
||||
void loop() override { store_value_(); }
|
||||
|
||||
void on_shutdown() override { store_value_(); }
|
||||
|
||||
@@ -145,4 +144,5 @@ template<typename T> T &id(GlobalsComponent<T> *value) { return value->value();
|
||||
template<typename T> T &id(RestoringGlobalsComponent<T> *value) { return value->value(); }
|
||||
template<typename T, uint8_t SZ> T &id(RestoringGlobalStringComponent<T, SZ> *value) { return value->value(); }
|
||||
|
||||
} // namespace esphome::globals
|
||||
} // namespace globals
|
||||
} // namespace esphome
|
||||
|
||||
@@ -6,6 +6,7 @@ from esphome.const import (
|
||||
CONF_BASELINE,
|
||||
CONF_CO2,
|
||||
CONF_ID,
|
||||
CONF_WARMUP_TIME,
|
||||
DEVICE_CLASS_CARBON_DIOXIDE,
|
||||
ICON_MOLECULE_CO2,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
@@ -14,8 +15,6 @@ from esphome.const import (
|
||||
|
||||
DEPENDENCIES = ["uart"]
|
||||
|
||||
CONF_WARMUP_TIME = "warmup_time"
|
||||
|
||||
hc8_ns = cg.esphome_ns.namespace("hc8")
|
||||
HC8Component = hc8_ns.class_("HC8Component", cg.PollingComponent, uart.UARTDevice)
|
||||
HC8CalibrateAction = hc8_ns.class_("HC8CalibrateAction", automation.Action)
|
||||
|
||||
@@ -107,7 +107,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Required(CONF_MAX_TEMPERATURE): cv.temperature,
|
||||
}
|
||||
),
|
||||
cv.only_with_arduino,
|
||||
cv.Any(cv.only_with_arduino, cv.only_on_esp32),
|
||||
)
|
||||
|
||||
|
||||
@@ -126,6 +126,6 @@ async def to_code(config):
|
||||
cg.add(var.set_max_temperature(config[CONF_MAX_TEMPERATURE]))
|
||||
cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE]))
|
||||
|
||||
cg.add_library("tonia/HeatpumpIR", "1.0.37")
|
||||
cg.add_library("tonia/HeatpumpIR", "1.0.40")
|
||||
if CORE.is_libretiny or CORE.is_esp32:
|
||||
CORE.add_platformio_option("lib_ignore", ["IRremoteESP8266"])
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "heatpumpir.h"
|
||||
|
||||
#ifdef USE_ARDUINO
|
||||
#if defined(USE_ARDUINO) || defined(USE_ESP32)
|
||||
|
||||
#include <map>
|
||||
#include "ir_sender_esphome.h"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_ARDUINO
|
||||
#if defined(USE_ARDUINO) || defined(USE_ESP32)
|
||||
|
||||
#include "esphome/components/climate_ir/climate_ir.h"
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "ir_sender_esphome.h"
|
||||
|
||||
#ifdef USE_ARDUINO
|
||||
#if defined(USE_ARDUINO) || defined(USE_ESP32)
|
||||
|
||||
namespace esphome {
|
||||
namespace heatpumpir {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_ARDUINO
|
||||
#if defined(USE_ARDUINO) || defined(USE_ESP32)
|
||||
|
||||
#include "esphome/components/remote_base/remote_base.h"
|
||||
#include <IRSender.h> // arduino-heatpump library
|
||||
|
||||
@@ -157,6 +157,7 @@ async def to_code(config):
|
||||
if CORE.is_esp32:
|
||||
cg.add(var.set_buffer_size_rx(config[CONF_BUFFER_SIZE_RX]))
|
||||
cg.add(var.set_buffer_size_tx(config[CONF_BUFFER_SIZE_TX]))
|
||||
cg.add(var.set_verify_ssl(config[CONF_VERIFY_SSL]))
|
||||
|
||||
if config.get(CONF_VERIFY_SSL):
|
||||
esp32.add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True)
|
||||
|
||||
@@ -89,7 +89,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
|
||||
config.max_redirection_count = this->redirect_limit_;
|
||||
config.auth_type = HTTP_AUTH_TYPE_BASIC;
|
||||
#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
|
||||
if (secure) {
|
||||
if (secure && this->verify_ssl_) {
|
||||
config.crt_bundle_attach = esp_crt_bundle_attach;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -34,6 +34,7 @@ class HttpRequestIDF : public HttpRequestComponent {
|
||||
|
||||
void set_buffer_size_rx(uint16_t buffer_size_rx) { this->buffer_size_rx_ = buffer_size_rx; }
|
||||
void set_buffer_size_tx(uint16_t buffer_size_tx) { this->buffer_size_tx_ = buffer_size_tx; }
|
||||
void set_verify_ssl(bool verify_ssl) { this->verify_ssl_ = verify_ssl; }
|
||||
|
||||
protected:
|
||||
std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, const std::string &body,
|
||||
@@ -42,6 +43,7 @@ class HttpRequestIDF : public HttpRequestComponent {
|
||||
// if zero ESP-IDF will use DEFAULT_HTTP_BUF_SIZE
|
||||
uint16_t buffer_size_rx_{};
|
||||
uint16_t buffer_size_tx_{};
|
||||
bool verify_ssl_{true};
|
||||
|
||||
/// @brief Monitors the http client events to gather response headers
|
||||
static esp_err_t http_event_handler(esp_http_client_event_t *evt);
|
||||
|
||||
@@ -223,7 +223,7 @@ async def to_code(config):
|
||||
var = cg.Pvariable(config[CONF_ID], rhs)
|
||||
|
||||
await display.register_display(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
|
||||
cg.add(var.set_dc_pin(dc))
|
||||
if init_sequences := config.get(CONF_INIT_SEQUENCE):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from esphome import codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_OPTIONS
|
||||
|
||||
@@ -24,6 +25,34 @@ from .label import CONF_LABEL
|
||||
CONF_DROPDOWN = "dropdown"
|
||||
CONF_DROPDOWN_LIST = "dropdown_list"
|
||||
|
||||
# Example valid dropdown symbol (left arrow) for error messages
|
||||
EXAMPLE_DROPDOWN_SYMBOL = "\U00002190" # ←
|
||||
|
||||
|
||||
def dropdown_symbol_validator(value):
|
||||
"""
|
||||
Validate that the dropdown symbol is a single Unicode character
|
||||
with a codepoint of 0x100 (256) or greater.
|
||||
This is required because LVGL uses codepoints below 0x100 for internal symbols.
|
||||
"""
|
||||
value = cv.string(value)
|
||||
# len(value) counts Unicode code points, not grapheme clusters or bytes
|
||||
if len(value) != 1:
|
||||
raise cv.Invalid(
|
||||
f"Dropdown symbol must be a single character, got '{value}' with length {len(value)}"
|
||||
)
|
||||
codepoint = ord(value)
|
||||
if codepoint < 0x100:
|
||||
# Format the example symbol as a Unicode escape for the error message
|
||||
example_escape = f"\\U{ord(EXAMPLE_DROPDOWN_SYMBOL):08X}"
|
||||
raise cv.Invalid(
|
||||
f"Dropdown symbol must have a Unicode codepoint of 0x100 (256) or greater. "
|
||||
f"'{value}' has codepoint {codepoint} (0x{codepoint:X}). "
|
||||
f"Use a character like '{example_escape}' ({EXAMPLE_DROPDOWN_SYMBOL}) or other Unicode symbols with codepoint >= 0x100."
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
lv_dropdown_t = LvSelect("LvDropdownType", parents=(LvCompound,))
|
||||
|
||||
lv_dropdown_list_t = LvType("lv_dropdown_list_t")
|
||||
@@ -33,7 +62,7 @@ dropdown_list_spec = WidgetType(
|
||||
|
||||
DROPDOWN_BASE_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_SYMBOL): lv_text,
|
||||
cv.Optional(CONF_SYMBOL): dropdown_symbol_validator,
|
||||
cv.Exclusive(CONF_SELECTED_INDEX, CONF_SELECTED_TEXT): lv_int,
|
||||
cv.Exclusive(CONF_SELECTED_TEXT, CONF_SELECTED_TEXT): lv_text,
|
||||
cv.Optional(CONF_DROPDOWN_LIST): part_schema(dropdown_list_spec.parts),
|
||||
@@ -70,7 +99,7 @@ class DropdownType(WidgetType):
|
||||
if options := config.get(CONF_OPTIONS):
|
||||
lv_add(w.var.set_options(options))
|
||||
if symbol := config.get(CONF_SYMBOL):
|
||||
lv.dropdown_set_symbol(w.var.obj, await lv_text.process(symbol))
|
||||
lv.dropdown_set_symbol(w.var.obj, cg.safe_exp(symbol))
|
||||
if (selected := config.get(CONF_SELECTED_INDEX)) is not None:
|
||||
value = await lv_int.process(selected)
|
||||
lv_add(w.var.set_selected_index(value, literal("LV_ANIM_OFF")))
|
||||
|
||||
@@ -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 {};
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ CONFIG_SCHEMA = (
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await spi.register_spi_device(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
await display.register_display(var, config)
|
||||
|
||||
cg.add(var.set_num_chips(config[CONF_NUM_CHIPS]))
|
||||
|
||||
@@ -86,7 +86,7 @@ CONFIG_SCHEMA = (
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await spi.register_spi_device(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
await display.register_display(var, config)
|
||||
|
||||
cg.add(var.set_num_chips(config[CONF_NUM_CHIPS]))
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -7,6 +7,7 @@ from esphome.const import (
|
||||
CONF_CO2,
|
||||
CONF_ID,
|
||||
CONF_TEMPERATURE,
|
||||
CONF_WARMUP_TIME,
|
||||
DEVICE_CLASS_CARBON_DIOXIDE,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
ICON_MOLECULE_CO2,
|
||||
@@ -18,7 +19,6 @@ from esphome.const import (
|
||||
DEPENDENCIES = ["uart"]
|
||||
|
||||
CONF_AUTOMATIC_BASELINE_CALIBRATION = "automatic_baseline_calibration"
|
||||
CONF_WARMUP_TIME = "warmup_time"
|
||||
CONF_DETECTION_RANGE = "detection_range"
|
||||
|
||||
mhz19_ns = cg.esphome_ns.namespace("mhz19")
|
||||
|
||||
@@ -260,7 +260,7 @@ async def to_code(config):
|
||||
cg.add(var.set_enable_pins(enable))
|
||||
|
||||
if CONF_SPI_ID in config:
|
||||
await spi.register_spi_device(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
sequence, madctl = model.get_sequence(config)
|
||||
cg.add(var.set_init_sequence(sequence))
|
||||
cg.add(var.set_madctl(madctl))
|
||||
|
||||
@@ -443,6 +443,4 @@ async def to_code(config):
|
||||
)
|
||||
cg.add(var.set_writer(lambda_))
|
||||
await display.register_display(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
# Displays are write-only, set the SPI device to write-only as well
|
||||
cg.add(var.set_write_only(True))
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -26,5 +26,3 @@ ST7789V.extend(
|
||||
reset_pin=40,
|
||||
invert_colors=True,
|
||||
)
|
||||
|
||||
models = {}
|
||||
|
||||
@@ -105,6 +105,3 @@ CO5300 = DriverChip(
|
||||
(WCE, 0x00),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
models = {}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
@@ -588,5 +588,3 @@ DriverChip(
|
||||
(0x29, 0x00),
|
||||
),
|
||||
)
|
||||
|
||||
models = {}
|
||||
|
||||
@@ -11,5 +11,3 @@ ST7789V.extend(
|
||||
dc_pin=21,
|
||||
reset_pin=18,
|
||||
)
|
||||
|
||||
models = {}
|
||||
|
||||
@@ -56,5 +56,3 @@ ST7796.extend(
|
||||
backlight_pin=48,
|
||||
invert_colors=True,
|
||||
)
|
||||
|
||||
models = {}
|
||||
|
||||
@@ -18,7 +18,7 @@ bool CustomMQTTDevice::publish(const std::string &topic, float value, int8_t num
|
||||
}
|
||||
bool CustomMQTTDevice::publish(const std::string &topic, int value) {
|
||||
char buffer[24];
|
||||
int len = snprintf(buffer, sizeof(buffer), "%d", value);
|
||||
size_t len = buf_append_printf(buffer, sizeof(buffer), 0, "%d", value);
|
||||
return global_mqtt_client->publish(topic, buffer, len);
|
||||
}
|
||||
bool CustomMQTTDevice::publish_json(const std::string &topic, const json::json_build_t &f, uint8_t qos, bool retain) {
|
||||
|
||||
@@ -43,7 +43,7 @@ void MQTTAlarmControlPanelComponent::setup() {
|
||||
|
||||
void MQTTAlarmControlPanelComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "MQTT alarm_control_panel '%s':", this->alarm_control_panel_->get_name().c_str());
|
||||
LOG_MQTT_COMPONENT(true, true)
|
||||
LOG_MQTT_COMPONENT(true, true);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Supported Features: %" PRIu32 "\n"
|
||||
" Requires Code to Disarm: %s\n"
|
||||
@@ -119,7 +119,8 @@ bool MQTTAlarmControlPanelComponent::publish_state() {
|
||||
default:
|
||||
state_s = "unknown";
|
||||
}
|
||||
return this->publish(this->get_state_topic_(), state_s);
|
||||
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
return this->publish(this->get_state_topic_to_(topic_buf), state_s);
|
||||
}
|
||||
|
||||
} // namespace esphome::mqtt
|
||||
|
||||
@@ -19,7 +19,7 @@ void MQTTBinarySensorComponent::setup() {
|
||||
|
||||
void MQTTBinarySensorComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "MQTT Binary Sensor '%s':", this->binary_sensor_->get_name().c_str());
|
||||
LOG_MQTT_COMPONENT(true, false)
|
||||
LOG_MQTT_COMPONENT(true, false);
|
||||
}
|
||||
MQTTBinarySensorComponent::MQTTBinarySensorComponent(binary_sensor::BinarySensor *binary_sensor)
|
||||
: binary_sensor_(binary_sensor) {
|
||||
@@ -52,8 +52,9 @@ bool MQTTBinarySensorComponent::publish_state(bool state) {
|
||||
if (this->binary_sensor_->is_status_binary_sensor())
|
||||
return true;
|
||||
|
||||
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
const char *state_s = state ? "ON" : "OFF";
|
||||
return this->publish(this->get_state_topic_(), state_s);
|
||||
return this->publish(this->get_state_topic_to_(topic_buf), state_s);
|
||||
}
|
||||
|
||||
} // namespace esphome::mqtt
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <utility>
|
||||
#include "esphome/components/network/util.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/entity_base.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/version.h"
|
||||
@@ -66,10 +67,13 @@ void MQTTClientComponent::setup() {
|
||||
"esphome/discover", [this](const std::string &topic, const std::string &payload) { this->send_device_info_(); },
|
||||
2);
|
||||
|
||||
std::string topic = "esphome/ping/";
|
||||
topic.append(App.get_name());
|
||||
// Format topic on stack - subscribe() copies it
|
||||
// "esphome/ping/" (13) + name (ESPHOME_DEVICE_NAME_MAX_LEN) + null (1)
|
||||
constexpr size_t ping_topic_buffer_size = 13 + ESPHOME_DEVICE_NAME_MAX_LEN + 1;
|
||||
char ping_topic[ping_topic_buffer_size];
|
||||
buf_append_printf(ping_topic, sizeof(ping_topic), 0, "esphome/ping/%s", App.get_name().c_str());
|
||||
this->subscribe(
|
||||
topic, [this](const std::string &topic, const std::string &payload) { this->send_device_info_(); }, 2);
|
||||
ping_topic, [this](const std::string &topic, const std::string &payload) { this->send_device_info_(); }, 2);
|
||||
}
|
||||
|
||||
if (this->enable_on_boot_) {
|
||||
@@ -81,8 +85,11 @@ void MQTTClientComponent::send_device_info_() {
|
||||
if (!this->is_connected() or !this->is_discovery_ip_enabled()) {
|
||||
return;
|
||||
}
|
||||
std::string topic = "esphome/discover/";
|
||||
topic.append(App.get_name());
|
||||
// Format topic on stack to avoid heap allocation
|
||||
// "esphome/discover/" (17) + name (ESPHOME_DEVICE_NAME_MAX_LEN) + null (1)
|
||||
constexpr size_t topic_buffer_size = 17 + ESPHOME_DEVICE_NAME_MAX_LEN + 1;
|
||||
char topic[topic_buffer_size];
|
||||
buf_append_printf(topic, sizeof(topic), 0, "esphome/discover/%s", App.get_name().c_str());
|
||||
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
this->publish_json(
|
||||
@@ -91,7 +98,17 @@ void MQTTClientComponent::send_device_info_() {
|
||||
uint8_t index = 0;
|
||||
for (auto &ip : network::get_ip_addresses()) {
|
||||
if (ip.is_set()) {
|
||||
root["ip" + (index == 0 ? "" : esphome::to_string(index))] = ip.str();
|
||||
char key[8]; // "ip" + up to 3 digits + null
|
||||
char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
if (index == 0) {
|
||||
key[0] = 'i';
|
||||
key[1] = 'p';
|
||||
key[2] = '\0';
|
||||
} else {
|
||||
buf_append_printf(key, sizeof(key), 0, "ip%u", index);
|
||||
}
|
||||
ip.str_to(ip_buf);
|
||||
root[key] = ip_buf;
|
||||
index++;
|
||||
}
|
||||
}
|
||||
@@ -396,6 +413,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 +523,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 +643,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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -27,20 +27,23 @@ inline char *append_char(char *p, char c) {
|
||||
// Max lengths for stack-based topic building.
|
||||
// These limits are enforced at Python config validation time in mqtt/__init__.py
|
||||
// using cv.Length() validators for topic_prefix and discovery_prefix.
|
||||
// MQTT_COMPONENT_TYPE_MAX_LEN and MQTT_SUFFIX_MAX_LEN are defined in mqtt_component.h.
|
||||
// MQTT_COMPONENT_TYPE_MAX_LEN, MQTT_SUFFIX_MAX_LEN, and MQTT_DEFAULT_TOPIC_MAX_LEN are in mqtt_component.h.
|
||||
// ESPHOME_DEVICE_NAME_MAX_LEN and OBJECT_ID_MAX_LEN are defined in entity_base.h.
|
||||
// This ensures the stack buffers below are always large enough.
|
||||
static constexpr size_t TOPIC_PREFIX_MAX_LEN = 64; // Validated in Python: cv.Length(max=64)
|
||||
static constexpr size_t DISCOVERY_PREFIX_MAX_LEN = 64; // Validated in Python: cv.Length(max=64)
|
||||
|
||||
// Stack buffer sizes - safe because all inputs are length-validated at config time
|
||||
// Format: prefix + "/" + type + "/" + object_id + "/" + suffix + null
|
||||
static constexpr size_t DEFAULT_TOPIC_MAX_LEN =
|
||||
TOPIC_PREFIX_MAX_LEN + 1 + MQTT_COMPONENT_TYPE_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 1 + MQTT_SUFFIX_MAX_LEN + 1;
|
||||
// Format: prefix + "/" + type + "/" + name + "/" + object_id + "/config" + null
|
||||
static constexpr size_t DISCOVERY_TOPIC_MAX_LEN = DISCOVERY_PREFIX_MAX_LEN + 1 + MQTT_COMPONENT_TYPE_MAX_LEN + 1 +
|
||||
ESPHOME_DEVICE_NAME_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 7 + 1;
|
||||
|
||||
// Function implementation of LOG_MQTT_COMPONENT macro to reduce code size
|
||||
void log_mqtt_component(const char *tag, MQTTComponent *obj, bool state_topic, bool command_topic) {
|
||||
char buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
if (state_topic)
|
||||
ESP_LOGCONFIG(tag, " State Topic: '%s'", obj->get_state_topic_to_(buf).c_str());
|
||||
if (command_topic)
|
||||
ESP_LOGCONFIG(tag, " Command Topic: '%s'", obj->get_command_topic_to_(buf).c_str());
|
||||
}
|
||||
|
||||
void MQTTComponent::set_qos(uint8_t qos) { this->qos_ = qos; }
|
||||
|
||||
void MQTTComponent::set_subscribe_qos(uint8_t qos) { this->subscribe_qos_ = qos; }
|
||||
@@ -69,19 +72,18 @@ std::string MQTTComponent::get_discovery_topic_(const MQTTDiscoveryInfo &discove
|
||||
return std::string(buf, p - buf);
|
||||
}
|
||||
|
||||
std::string MQTTComponent::get_default_topic_for_(const std::string &suffix) const {
|
||||
StringRef MQTTComponent::get_default_topic_for_to_(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf, const char *suffix,
|
||||
size_t suffix_len) const {
|
||||
const std::string &topic_prefix = global_mqtt_client->get_topic_prefix();
|
||||
if (topic_prefix.empty()) {
|
||||
// If the topic_prefix is null, the default topic should be null
|
||||
return "";
|
||||
return StringRef(); // Empty topic_prefix means no default topic
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
char buf[DEFAULT_TOPIC_MAX_LEN];
|
||||
char *p = buf;
|
||||
char *p = buf.data();
|
||||
|
||||
p = append_str(p, topic_prefix.data(), topic_prefix.size());
|
||||
p = append_char(p, '/');
|
||||
@@ -89,35 +91,70 @@ std::string MQTTComponent::get_default_topic_for_(const std::string &suffix) con
|
||||
p = append_char(p, '/');
|
||||
p = append_str(p, object_id.c_str(), object_id.size());
|
||||
p = append_char(p, '/');
|
||||
p = append_str(p, suffix.data(), suffix.size());
|
||||
p = append_str(p, suffix, suffix_len);
|
||||
*p = '\0';
|
||||
|
||||
return std::string(buf, p - buf);
|
||||
return StringRef(buf.data(), p - buf.data());
|
||||
}
|
||||
|
||||
std::string MQTTComponent::get_default_topic_for_(const std::string &suffix) const {
|
||||
char buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
StringRef ref = this->get_default_topic_for_to_(buf, suffix.data(), suffix.size());
|
||||
return std::string(ref.c_str(), ref.size());
|
||||
}
|
||||
|
||||
StringRef MQTTComponent::get_state_topic_to_(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf) const {
|
||||
if (this->custom_state_topic_.has_value()) {
|
||||
// Returns ref to existing data for static/value, uses buf only for lambda case
|
||||
return this->custom_state_topic_.ref_or_copy_to(buf.data(), buf.size());
|
||||
}
|
||||
return this->get_default_topic_for_to_(buf, "state", 5);
|
||||
}
|
||||
|
||||
StringRef MQTTComponent::get_command_topic_to_(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf) const {
|
||||
if (this->custom_command_topic_.has_value()) {
|
||||
// Returns ref to existing data for static/value, uses buf only for lambda case
|
||||
return this->custom_command_topic_.ref_or_copy_to(buf.data(), buf.size());
|
||||
}
|
||||
return this->get_default_topic_for_to_(buf, "command", 7);
|
||||
}
|
||||
|
||||
std::string MQTTComponent::get_state_topic_() const {
|
||||
if (this->custom_state_topic_.has_value())
|
||||
return this->custom_state_topic_.value();
|
||||
return this->get_default_topic_for_("state");
|
||||
char buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
StringRef ref = this->get_state_topic_to_(buf);
|
||||
return std::string(ref.c_str(), ref.size());
|
||||
}
|
||||
|
||||
std::string MQTTComponent::get_command_topic_() const {
|
||||
if (this->custom_command_topic_.has_value())
|
||||
return this->custom_command_topic_.value();
|
||||
return this->get_default_topic_for_("command");
|
||||
char buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
StringRef ref = this->get_command_topic_to_(buf);
|
||||
return std::string(ref.c_str(), ref.size());
|
||||
}
|
||||
|
||||
bool MQTTComponent::publish(const std::string &topic, const std::string &payload) {
|
||||
return this->publish(topic, payload.data(), payload.size());
|
||||
return this->publish(topic.c_str(), payload.data(), payload.size());
|
||||
}
|
||||
|
||||
bool MQTTComponent::publish(const std::string &topic, const char *payload, size_t payload_length) {
|
||||
if (topic.empty())
|
||||
return this->publish(topic.c_str(), payload, payload_length);
|
||||
}
|
||||
|
||||
bool MQTTComponent::publish(const char *topic, const char *payload, size_t payload_length) {
|
||||
if (topic[0] == '\0')
|
||||
return false;
|
||||
return global_mqtt_client->publish(topic, payload, payload_length, this->qos_, this->retain_);
|
||||
}
|
||||
|
||||
bool MQTTComponent::publish(const char *topic, const char *payload) {
|
||||
return this->publish(topic, payload, strlen(payload));
|
||||
}
|
||||
|
||||
bool MQTTComponent::publish_json(const std::string &topic, const json::json_build_t &f) {
|
||||
if (topic.empty())
|
||||
return this->publish_json(topic.c_str(), f);
|
||||
}
|
||||
|
||||
bool MQTTComponent::publish_json(const char *topic, const json::json_build_t &f) {
|
||||
if (topic[0] == '\0')
|
||||
return false;
|
||||
return global_mqtt_client->publish_json(topic, f, this->qos_, this->retain_);
|
||||
}
|
||||
@@ -168,10 +205,14 @@ bool MQTTComponent::send_discovery_() {
|
||||
break;
|
||||
}
|
||||
|
||||
if (config.state_topic)
|
||||
root[MQTT_STATE_TOPIC] = this->get_state_topic_();
|
||||
if (config.command_topic)
|
||||
root[MQTT_COMMAND_TOPIC] = this->get_command_topic_();
|
||||
if (config.state_topic) {
|
||||
char state_topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
root[MQTT_STATE_TOPIC] = this->get_state_topic_to_(state_topic_buf);
|
||||
}
|
||||
if (config.command_topic) {
|
||||
char command_topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
root[MQTT_COMMAND_TOPIC] = this->get_command_topic_to_(command_topic_buf);
|
||||
}
|
||||
if (this->command_retain_)
|
||||
root[MQTT_COMMAND_RETAIN] = true;
|
||||
|
||||
@@ -190,27 +231,37 @@ bool MQTTComponent::send_discovery_() {
|
||||
StringRef object_id = this->get_default_object_id_to_(object_id_buf);
|
||||
if (discovery_info.unique_id_generator == MQTT_MAC_ADDRESS_UNIQUE_ID_GENERATOR) {
|
||||
char friendly_name_hash[9];
|
||||
snprintf(friendly_name_hash, sizeof(friendly_name_hash), "%08" PRIx32, fnv1_hash(this->friendly_name_()));
|
||||
buf_append_printf(friendly_name_hash, sizeof(friendly_name_hash), 0, "%08" PRIx32,
|
||||
fnv1_hash(this->friendly_name_()));
|
||||
// Format: mac-component_type-hash (e.g. "aabbccddeeff-sensor-12345678")
|
||||
// MAC (12) + "-" (1) + domain (max 20) + "-" (1) + hash (8) + null (1) = 43
|
||||
char unique_id[MAC_ADDRESS_BUFFER_SIZE + ESPHOME_DOMAIN_MAX_LEN + 11];
|
||||
char mac_buf[MAC_ADDRESS_BUFFER_SIZE];
|
||||
get_mac_address_into_buffer(mac_buf);
|
||||
snprintf(unique_id, sizeof(unique_id), "%s-%s-%s", mac_buf, this->component_type(), friendly_name_hash);
|
||||
buf_append_printf(unique_id, sizeof(unique_id), 0, "%s-%s-%s", mac_buf, this->component_type(),
|
||||
friendly_name_hash);
|
||||
root[MQTT_UNIQUE_ID] = unique_id;
|
||||
} else {
|
||||
// default to almost-unique ID. It's a hack but the only way to get that
|
||||
// gorgeous device registry view.
|
||||
root[MQTT_UNIQUE_ID] = "ESP" + std::string(this->component_type()) + object_id.c_str();
|
||||
// "ESP" (3) + component_type (max 20) + object_id (max 128) + null
|
||||
char unique_id_buf[3 + MQTT_COMPONENT_TYPE_MAX_LEN + OBJECT_ID_MAX_LEN + 1];
|
||||
buf_append_printf(unique_id_buf, sizeof(unique_id_buf), 0, "ESP%s%s", this->component_type(),
|
||||
object_id.c_str());
|
||||
root[MQTT_UNIQUE_ID] = unique_id_buf;
|
||||
}
|
||||
|
||||
const std::string &node_name = App.get_name();
|
||||
if (discovery_info.object_id_generator == MQTT_DEVICE_NAME_OBJECT_ID_GENERATOR)
|
||||
root[MQTT_OBJECT_ID] = node_name + "_" + object_id.c_str();
|
||||
if (discovery_info.object_id_generator == MQTT_DEVICE_NAME_OBJECT_ID_GENERATOR) {
|
||||
// node_name (max 31) + "_" (1) + object_id (max 128) + null
|
||||
char object_id_full[ESPHOME_DEVICE_NAME_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 1];
|
||||
buf_append_printf(object_id_full, sizeof(object_id_full), 0, "%s_%s", node_name.c_str(), object_id.c_str());
|
||||
root[MQTT_OBJECT_ID] = object_id_full;
|
||||
}
|
||||
|
||||
const std::string &friendly_name_ref = App.get_friendly_name();
|
||||
const std::string &node_friendly_name = friendly_name_ref.empty() ? node_name : friendly_name_ref;
|
||||
std::string node_area = App.get_area();
|
||||
const char *node_area = App.get_area();
|
||||
|
||||
JsonObject device_info = root[MQTT_DEVICE].to<JsonObject>();
|
||||
char mac[MAC_ADDRESS_BUFFER_SIZE];
|
||||
@@ -221,18 +272,29 @@ bool MQTTComponent::send_discovery_() {
|
||||
device_info[MQTT_DEVICE_SW_VERSION] = ESPHOME_PROJECT_VERSION " (ESPHome " ESPHOME_VERSION ")";
|
||||
const char *model = std::strchr(ESPHOME_PROJECT_NAME, '.');
|
||||
device_info[MQTT_DEVICE_MODEL] = model == nullptr ? ESPHOME_BOARD : model + 1;
|
||||
device_info[MQTT_DEVICE_MANUFACTURER] =
|
||||
model == nullptr ? ESPHOME_PROJECT_NAME : std::string(ESPHOME_PROJECT_NAME, model - ESPHOME_PROJECT_NAME);
|
||||
if (model == nullptr) {
|
||||
device_info[MQTT_DEVICE_MANUFACTURER] = ESPHOME_PROJECT_NAME;
|
||||
} else {
|
||||
// Extract manufacturer (part before '.') using stack buffer to avoid heap allocation
|
||||
// memcpy is used instead of strncpy since we know the exact length and strncpy
|
||||
// would still require manual null-termination
|
||||
char manufacturer[sizeof(ESPHOME_PROJECT_NAME)];
|
||||
size_t len = model - ESPHOME_PROJECT_NAME;
|
||||
memcpy(manufacturer, ESPHOME_PROJECT_NAME, len);
|
||||
manufacturer[len] = '\0';
|
||||
device_info[MQTT_DEVICE_MANUFACTURER] = manufacturer;
|
||||
}
|
||||
#else
|
||||
static const char ver_fmt[] PROGMEM = ESPHOME_VERSION " (config hash 0x%08" PRIx32 ")";
|
||||
// Buffer sized for format string expansion: ~4 bytes net growth from format specifier to 8 hex digits, plus
|
||||
// safety margin
|
||||
char version_buf[sizeof(ver_fmt) + 8];
|
||||
#ifdef USE_ESP8266
|
||||
char fmt_buf[sizeof(ver_fmt)];
|
||||
strcpy_P(fmt_buf, ver_fmt);
|
||||
const char *fmt = fmt_buf;
|
||||
snprintf_P(version_buf, sizeof(version_buf), ver_fmt, App.get_config_hash());
|
||||
#else
|
||||
const char *fmt = ver_fmt;
|
||||
snprintf(version_buf, sizeof(version_buf), ver_fmt, App.get_config_hash());
|
||||
#endif
|
||||
device_info[MQTT_DEVICE_SW_VERSION] = str_sprintf(fmt, App.get_config_hash());
|
||||
device_info[MQTT_DEVICE_SW_VERSION] = version_buf;
|
||||
device_info[MQTT_DEVICE_MODEL] = ESPHOME_BOARD;
|
||||
#if defined(USE_ESP8266) || defined(USE_ESP32)
|
||||
device_info[MQTT_DEVICE_MANUFACTURER] = "Espressif";
|
||||
@@ -246,7 +308,7 @@ bool MQTTComponent::send_discovery_() {
|
||||
device_info[MQTT_DEVICE_MANUFACTURER] = "Host";
|
||||
#endif
|
||||
#endif
|
||||
if (!node_area.empty()) {
|
||||
if (node_area[0] != '\0') {
|
||||
device_info[MQTT_DEVICE_SUGGESTED_AREA] = node_area;
|
||||
}
|
||||
|
||||
@@ -288,7 +350,9 @@ void MQTTComponent::set_availability(std::string topic, std::string payload_avai
|
||||
}
|
||||
void MQTTComponent::disable_availability() { this->set_availability("", "", ""); }
|
||||
void MQTTComponent::call_setup() {
|
||||
if (this->is_internal())
|
||||
// Cache is_internal result once during setup - topics don't change after this
|
||||
this->is_internal_ = this->compute_is_internal_();
|
||||
if (this->is_internal_)
|
||||
return;
|
||||
|
||||
this->setup();
|
||||
@@ -308,16 +372,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_()) {
|
||||
@@ -344,26 +404,28 @@ StringRef MQTTComponent::get_default_object_id_to_(std::span<char, OBJECT_ID_MAX
|
||||
}
|
||||
StringRef MQTTComponent::get_icon_ref_() const { return this->get_entity()->get_icon_ref(); }
|
||||
bool MQTTComponent::is_disabled_by_default_() const { return this->get_entity()->is_disabled_by_default(); }
|
||||
bool MQTTComponent::is_internal() {
|
||||
bool MQTTComponent::compute_is_internal_() {
|
||||
if (this->custom_state_topic_.has_value()) {
|
||||
// If the custom state_topic is null, return true as it is internal and should not publish
|
||||
// If the custom state_topic is empty, return true as it is internal and should not publish
|
||||
// else, return false, as it is explicitly set to a topic, so it is not internal and should publish
|
||||
return this->get_state_topic_().empty();
|
||||
// Using is_empty() avoids heap allocation for non-lambda cases
|
||||
return this->custom_state_topic_.is_empty();
|
||||
}
|
||||
|
||||
if (this->custom_command_topic_.has_value()) {
|
||||
// If the custom command_topic is null, return true as it is internal and should not publish
|
||||
// If the custom command_topic is empty, return true as it is internal and should not publish
|
||||
// else, return false, as it is explicitly set to a topic, so it is not internal and should publish
|
||||
return this->get_command_topic_().empty();
|
||||
// Using is_empty() avoids heap allocation for non-lambda cases
|
||||
return this->custom_command_topic_.is_empty();
|
||||
}
|
||||
|
||||
// No custom topics have been set
|
||||
if (this->get_default_topic_for_("").empty()) {
|
||||
// If the default topic prefix is null, then the component, by default, is internal and should not publish
|
||||
// No custom topics have been set - check topic_prefix directly to avoid allocation
|
||||
if (global_mqtt_client->get_topic_prefix().empty()) {
|
||||
// If the default topic prefix is empty, then the component, by default, is internal and should not publish
|
||||
return true;
|
||||
}
|
||||
|
||||
// Use ESPHome's component internal state if topic_prefix is not null with no custom state_topic or command_topic
|
||||
// Use ESPHome's component internal state if topic_prefix is not empty with no custom state_topic or command_topic
|
||||
return this->get_entity()->is_internal();
|
||||
}
|
||||
|
||||
|
||||
@@ -20,17 +20,22 @@ struct SendDiscoveryConfig {
|
||||
bool command_topic{true}; ///< If the command topic should be included. Default to true.
|
||||
};
|
||||
|
||||
// Max lengths for stack-based topic building (must match mqtt_component.cpp)
|
||||
// Max lengths for stack-based topic building.
|
||||
// These limits are enforced at Python config validation time in mqtt/__init__.py
|
||||
// using cv.Length() validators for topic_prefix and discovery_prefix.
|
||||
// This ensures the stack buffers are always large enough.
|
||||
static constexpr size_t MQTT_COMPONENT_TYPE_MAX_LEN = 20;
|
||||
static constexpr size_t MQTT_SUFFIX_MAX_LEN = 32;
|
||||
static constexpr size_t MQTT_TOPIC_PREFIX_MAX_LEN = 64; // Validated in Python: cv.Length(max=64)
|
||||
// Stack buffer size - safe because all inputs are length-validated at config time
|
||||
// Format: prefix + "/" + type + "/" + object_id + "/" + suffix + null
|
||||
static constexpr size_t MQTT_DEFAULT_TOPIC_MAX_LEN =
|
||||
MQTT_TOPIC_PREFIX_MAX_LEN + 1 + MQTT_COMPONENT_TYPE_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 1 + MQTT_SUFFIX_MAX_LEN + 1;
|
||||
|
||||
#define LOG_MQTT_COMPONENT(state_topic, command_topic) \
|
||||
if (state_topic) { \
|
||||
ESP_LOGCONFIG(TAG, " State Topic: '%s'", this->get_state_topic_().c_str()); \
|
||||
} \
|
||||
if (command_topic) { \
|
||||
ESP_LOGCONFIG(TAG, " Command Topic: '%s'", this->get_command_topic_().c_str()); \
|
||||
}
|
||||
class MQTTComponent; // Forward declaration
|
||||
void log_mqtt_component(const char *tag, MQTTComponent *obj, bool state_topic, bool command_topic);
|
||||
|
||||
#define LOG_MQTT_COMPONENT(state_topic, command_topic) log_mqtt_component(TAG, this, state_topic, command_topic)
|
||||
|
||||
// Macro to define component_type() with compile-time length verification
|
||||
// Usage: MQTT_COMPONENT_TYPE(MQTTSensorComponent, "sensor")
|
||||
@@ -74,6 +79,8 @@ static constexpr size_t MQTT_SUFFIX_MAX_LEN = 32;
|
||||
* a clean separation.
|
||||
*/
|
||||
class MQTTComponent : public Component {
|
||||
friend void log_mqtt_component(const char *tag, MQTTComponent *obj, bool state_topic, bool command_topic);
|
||||
|
||||
public:
|
||||
/// Constructs a MQTTComponent.
|
||||
explicit MQTTComponent();
|
||||
@@ -81,8 +88,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.
|
||||
@@ -90,7 +95,8 @@ class MQTTComponent : public Component {
|
||||
|
||||
virtual bool send_initial_state() = 0;
|
||||
|
||||
virtual bool is_internal();
|
||||
/// Returns cached is_internal result (computed once during setup).
|
||||
bool is_internal() const { return this->is_internal_; }
|
||||
|
||||
/// Set QOS for state messages.
|
||||
void set_qos(uint8_t qos);
|
||||
@@ -133,6 +139,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.
|
||||
@@ -148,6 +157,38 @@ class MQTTComponent : public Component {
|
||||
*/
|
||||
bool publish(const std::string &topic, const char *payload, size_t payload_length);
|
||||
|
||||
/** Send a MQTT message (no heap allocation for topic).
|
||||
*
|
||||
* @param topic The topic as C string.
|
||||
* @param payload The payload buffer.
|
||||
* @param payload_length The length of the payload.
|
||||
*/
|
||||
bool publish(const char *topic, const char *payload, size_t payload_length);
|
||||
|
||||
/** Send a MQTT message (no heap allocation for topic).
|
||||
*
|
||||
* @param topic The topic as StringRef (for use with get_state_topic_to_()).
|
||||
* @param payload The payload buffer.
|
||||
* @param payload_length The length of the payload.
|
||||
*/
|
||||
bool publish(StringRef topic, const char *payload, size_t payload_length) {
|
||||
return this->publish(topic.c_str(), payload, payload_length);
|
||||
}
|
||||
|
||||
/** Send a MQTT message (no heap allocation for topic).
|
||||
*
|
||||
* @param topic The topic as C string.
|
||||
* @param payload The null-terminated payload.
|
||||
*/
|
||||
bool publish(const char *topic, const char *payload);
|
||||
|
||||
/** Send a MQTT message (no heap allocation for topic).
|
||||
*
|
||||
* @param topic The topic as StringRef (for use with get_state_topic_to_()).
|
||||
* @param payload The null-terminated payload.
|
||||
*/
|
||||
bool publish(StringRef topic, const char *payload) { return this->publish(topic.c_str(), payload); }
|
||||
|
||||
/** Construct and send a JSON MQTT message.
|
||||
*
|
||||
* @param topic The topic.
|
||||
@@ -155,6 +196,20 @@ class MQTTComponent : public Component {
|
||||
*/
|
||||
bool publish_json(const std::string &topic, const json::json_build_t &f);
|
||||
|
||||
/** Construct and send a JSON MQTT message (no heap allocation for topic).
|
||||
*
|
||||
* @param topic The topic as C string.
|
||||
* @param f The Json Message builder.
|
||||
*/
|
||||
bool publish_json(const char *topic, const json::json_build_t &f);
|
||||
|
||||
/** Construct and send a JSON MQTT message (no heap allocation for topic).
|
||||
*
|
||||
* @param topic The topic as StringRef (for use with get_state_topic_to_()).
|
||||
* @param f The Json Message builder.
|
||||
*/
|
||||
bool publish_json(StringRef topic, const json::json_build_t &f) { return this->publish_json(topic.c_str(), f); }
|
||||
|
||||
/** Subscribe to a MQTT topic.
|
||||
*
|
||||
* @param topic The topic. Wildcards are currently not supported.
|
||||
@@ -178,7 +233,16 @@ class MQTTComponent : public Component {
|
||||
/// Helper method to get the discovery topic for this component.
|
||||
std::string get_discovery_topic_(const MQTTDiscoveryInfo &discovery_info) const;
|
||||
|
||||
/** Get this components state/command/... topic.
|
||||
/** Get this components state/command/... topic into a buffer.
|
||||
*
|
||||
* @param buf The buffer to write to (must be exactly MQTT_DEFAULT_TOPIC_MAX_LEN).
|
||||
* @param suffix The suffix/key such as "state" or "command".
|
||||
* @return StringRef pointing to the buffer with the topic.
|
||||
*/
|
||||
StringRef get_default_topic_for_to_(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf, const char *suffix,
|
||||
size_t suffix_len) const;
|
||||
|
||||
/** Get this components state/command/... topic (allocates std::string).
|
||||
*
|
||||
* @param suffix The suffix/key such as "state" or "command".
|
||||
* @return The full topic.
|
||||
@@ -199,10 +263,20 @@ class MQTTComponent : public Component {
|
||||
/// Get whether the underlying Entity is disabled by default
|
||||
bool is_disabled_by_default_() const;
|
||||
|
||||
/// Get the MQTT topic that new states will be shared to.
|
||||
/// Get the MQTT state topic into a buffer (no heap allocation for non-lambda custom topics).
|
||||
/// @param buf Buffer of exactly MQTT_DEFAULT_TOPIC_MAX_LEN bytes.
|
||||
/// @return StringRef pointing to the topic in the buffer.
|
||||
StringRef get_state_topic_to_(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf) const;
|
||||
|
||||
/// Get the MQTT command topic into a buffer (no heap allocation for non-lambda custom topics).
|
||||
/// @param buf Buffer of exactly MQTT_DEFAULT_TOPIC_MAX_LEN bytes.
|
||||
/// @return StringRef pointing to the topic in the buffer.
|
||||
StringRef get_command_topic_to_(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf) const;
|
||||
|
||||
/// Get the MQTT topic that new states will be shared to (allocates std::string).
|
||||
std::string get_state_topic_() const;
|
||||
|
||||
/// Get the MQTT topic for listening to commands.
|
||||
/// Get the MQTT topic for listening to commands (allocates std::string).
|
||||
std::string get_command_topic_() const;
|
||||
|
||||
bool is_connected_() const;
|
||||
@@ -220,12 +294,18 @@ class MQTTComponent : public Component {
|
||||
|
||||
std::unique_ptr<Availability> availability_;
|
||||
|
||||
bool command_retain_{false};
|
||||
bool retain_{true};
|
||||
uint8_t qos_{0};
|
||||
uint8_t subscribe_qos_{0};
|
||||
bool discovery_enabled_{true};
|
||||
bool resend_state_{false};
|
||||
// Packed bitfields - QoS values are 0-2, bools are flags
|
||||
uint8_t qos_ : 2 {0};
|
||||
uint8_t subscribe_qos_ : 2 {0};
|
||||
bool command_retain_ : 1 {false};
|
||||
bool retain_ : 1 {true};
|
||||
bool discovery_enabled_ : 1 {true};
|
||||
bool resend_state_ : 1 {false};
|
||||
bool is_internal_ : 1 {false}; ///< Cached result of compute_is_internal_(), set during setup
|
||||
|
||||
/// Compute is_internal status based on topics and entity state.
|
||||
/// Called once during setup to cache the result.
|
||||
bool compute_is_internal_();
|
||||
};
|
||||
|
||||
} // namespace esphome::mqtt
|
||||
|
||||
@@ -51,7 +51,7 @@ void MQTTCoverComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "MQTT cover '%s':", this->cover_->get_name().c_str());
|
||||
auto traits = this->cover_->get_traits();
|
||||
bool has_command_topic = traits.get_supports_position() || !traits.get_supports_tilt();
|
||||
LOG_MQTT_COMPONENT(true, has_command_topic)
|
||||
LOG_MQTT_COMPONENT(true, has_command_topic);
|
||||
if (traits.get_supports_position()) {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Position State Topic: '%s'\n"
|
||||
@@ -115,7 +115,8 @@ bool MQTTCoverComponent::publish_state() {
|
||||
: this->cover_->position == COVER_OPEN ? "open"
|
||||
: traits.get_supports_position() ? "open"
|
||||
: "unknown";
|
||||
if (!this->publish(this->get_state_topic_(), state_s))
|
||||
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
if (!this->publish(this->get_state_topic_to_(topic_buf), state_s))
|
||||
success = false;
|
||||
return success;
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ void MQTTDateComponent::setup() {
|
||||
|
||||
void MQTTDateComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "MQTT Date '%s':", this->date_->get_name().c_str());
|
||||
LOG_MQTT_COMPONENT(true, true)
|
||||
LOG_MQTT_COMPONENT(true, true);
|
||||
}
|
||||
|
||||
MQTT_COMPONENT_TYPE(MQTTDateComponent, "date")
|
||||
@@ -53,7 +53,8 @@ bool MQTTDateComponent::send_initial_state() {
|
||||
}
|
||||
}
|
||||
bool MQTTDateComponent::publish_state(uint16_t year, uint8_t month, uint8_t day) {
|
||||
return this->publish_json(this->get_state_topic_(), [year, month, day](JsonObject root) {
|
||||
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
return this->publish_json(this->get_state_topic_to_(topic_buf), [year, month, day](JsonObject root) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
root[ESPHOME_F("year")] = year;
|
||||
root[ESPHOME_F("month")] = month;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user