Merge branch 'dev' into ota-so-rcvtimeo

This commit is contained in:
J. Nick Koston
2026-03-11 20:29:47 -10:00
committed by GitHub
300 changed files with 6692 additions and 1176 deletions
+2 -2
View File
@@ -945,13 +945,13 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Download target analysis JSON
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: memory-analysis-target
path: ./memory-analysis
continue-on-error: true
- name: Download PR analysis JSON
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: memory-analysis-pr
path: ./memory-analysis
@@ -10,6 +10,9 @@ name: Codeowner Approved Label
on:
pull_request_target:
types: [opened, synchronize, reopened, ready_for_review]
branches-ignore:
- release
- beta
permissions:
issues: write
@@ -13,6 +13,9 @@ on:
# Needs to be pull_request_target to get write permissions
pull_request_target:
types: [opened, reopened, synchronize, ready_for_review]
branches-ignore:
- release
- beta
permissions:
pull-requests: write
+12
View File
@@ -65,6 +65,18 @@ jobs:
return;
}
// Check for angle brackets not wrapped in backticks.
// Astro docs MDX treats bare < as JSX component opening tags.
const stripped = title.replace(/`[^`]*`/g, '');
if (/[<>]/.test(stripped)) {
core.setFailed(
'PR title contains `<` or `>` not wrapped in backticks.\n' +
'Astro docs MDX interprets bare `<` as JSX components.\n' +
'Please wrap angle brackets with backticks, e.g.: [component] Add `<feature>` support'
);
return;
}
// Check title starts with [tag] prefix
const bracketPattern = /^\[\w+\]/;
if (!bracketPattern.test(title)) {
+1 -1
View File
@@ -171,7 +171,7 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Download digests
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: digests-*
path: /tmp/digests
+1 -1
View File
@@ -37,7 +37,7 @@ repos:
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/asottile/pyupgrade
rev: v3.20.0
rev: v3.21.2
hooks:
- id: pyupgrade
args: [--py311-plus]
+3
View File
@@ -132,6 +132,7 @@ esphome/components/dashboard_import/* @esphome/core
esphome/components/datetime/* @jesserockz @rfdarter
esphome/components/debug/* @esphome/core
esphome/components/delonghi/* @grob6000
esphome/components/dew_point/* @CFlix
esphome/components/dfplayer/* @glmnet
esphome/components/dfrobot_sen0395/* @niklasweber
esphome/components/dht/* @OttoWinter
@@ -410,6 +411,7 @@ esphome/components/restart/* @esphome/core
esphome/components/rf_bridge/* @jesserockz
esphome/components/rgbct/* @jesserockz
esphome/components/rp2040/* @jesserockz
esphome/components/rp2040_ble/* @bdraco
esphome/components/rp2040_pio_led_strip/* @Papa-DMan
esphome/components/rp2040_pwm/* @jesserockz
esphome/components/rpi_dpi_rgb/* @clydebarrow
@@ -458,6 +460,7 @@ esphome/components/sonoff_d1/* @anatoly-savchenkov
esphome/components/sound_level/* @kahrendt
esphome/components/speaker/* @jesserockz @kahrendt
esphome/components/speaker/media_player/* @kahrendt @synesthesiam
esphome/components/speaker_source/* @kahrendt
esphome/components/spi/* @clydebarrow @esphome/core
esphome/components/spi_device/* @clydebarrow
esphome/components/spi_led_strip/* @clydebarrow
+1 -1
View File
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version
# control system is used.
PROJECT_NUMBER = 2026.3.0-dev
PROJECT_NUMBER = 2026.4.0-dev
# Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a
+274 -10
View File
@@ -1,6 +1,7 @@
# PYTHON_ARGCOMPLETE_OK
import argparse
from collections.abc import Callable
from contextlib import suppress
from datetime import datetime
import functools
import getpass
@@ -9,6 +10,8 @@ import logging
import os
from pathlib import Path
import re
import shutil
import subprocess
import sys
import time
from typing import Protocol
@@ -44,7 +47,9 @@ from esphome.const import (
CONF_SUBSTITUTIONS,
CONF_TOPIC,
ENV_NOGITIGNORE,
KEY_CORE,
KEY_NATIVE_IDF,
KEY_TARGET_PLATFORM,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_RP2040,
@@ -56,7 +61,11 @@ from esphome.helpers import get_bool_env, indent, is_ip_address
from esphome.log import AnsiFore, color, setup_log
from esphome.types import ConfigType
from esphome.util import (
PICOTOOL_PACKAGE,
detect_rp2040_bootsel,
get_picotool_path,
get_serial_ports,
is_picotool_usb_permission_error,
list_yaml_files,
run_external_command,
run_external_process,
@@ -65,9 +74,26 @@ from esphome.util import (
_LOGGER = logging.getLogger(__name__)
ESPHOME_COMMAND = [sys.executable, "-m", "esphome"]
# Maximum buffer size for serial log reading to prevent unbounded memory growth
SERIAL_BUFFER_MAX_SIZE = 65536
_RP2040_BOOTSEL_INSTRUCTIONS = (
"To enter BOOTSEL mode:\n"
" 1. Unplug the device\n"
" 2. Hold the BOOT/BOOTSEL button\n"
" 3. Plug in the USB cable while holding the button\n"
" 4. Release the button - the device should appear as a USB drive (RPI-RP2)\n"
"Then run the upload command again."
)
_RP2040_UDEV_HINT = (
"You may need to add a udev rule for RP2040 devices. "
"See: https://github.com/raspberrypi/picotool"
"/blob/master/udev/60-picotool.rules"
)
# Special non-component keys that appear in configs
_NON_COMPONENT_KEYS = frozenset(
{
@@ -163,6 +189,7 @@ class PortType(StrEnum):
NETWORK = "NETWORK"
MQTT = "MQTT"
MQTTIP = "MQTTIP"
BOOTSEL = "BOOTSEL"
# Magic MQTT port types that require special handling
@@ -241,6 +268,19 @@ def choose_upload_log_host(
(f"{port.path} ({port.description})", port.path) for port in get_serial_ports()
]
# Add RP2040 BOOTSEL device option when uploading
bootsel_permission_error = False
if (
purpose == Purpose.UPLOADING
and CORE.data.get(KEY_CORE, {}).get(KEY_TARGET_PLATFORM) == PLATFORM_RP2040
and (picotool := _find_picotool()) is not None
):
bootsel = detect_rp2040_bootsel(picotool)
if bootsel.device_count > 0:
options.append(("RP2040 BOOTSEL (via picotool)", "BOOTSEL"))
elif bootsel.permission_error:
bootsel_permission_error = True
if purpose == Purpose.LOGGING:
if has_mqtt_logging():
mqtt_config = CORE.config[CONF_MQTT]
@@ -258,6 +298,25 @@ def choose_upload_log_host(
if has_mqtt_ip_lookup():
options.append(("Over The Air (MQTT IP lookup)", "MQTTIP"))
# Show helpful BOOTSEL instructions for RP2040 when no BOOTSEL device is found
if (
purpose == Purpose.UPLOADING
and CORE.data.get(KEY_CORE, {}).get(KEY_TARGET_PLATFORM) == PLATFORM_RP2040
and not any(get_port_type(opt[1]) == PortType.BOOTSEL for opt in options)
):
if bootsel_permission_error:
_LOGGER.warning(
"An RP2040 device in BOOTSEL mode was detected but could "
"not be accessed due to USB permissions."
)
if sys.platform.startswith("linux"):
_LOGGER.warning(_RP2040_UDEV_HINT)
if not options:
raise EsphomeError(
f"No RP2040 device found. {_RP2040_BOOTSEL_INSTRUCTIONS}"
)
_LOGGER.info("Tip: %s", _RP2040_BOOTSEL_INSTRUCTIONS)
if check_default is not None and check_default in [opt[1] for opt in options]:
return [check_default]
return [choose_prompt(options, purpose=purpose)]
@@ -404,10 +463,13 @@ def get_port_type(port: str) -> PortType:
Returns:
PortType.SERIAL for serial ports (/dev/ttyUSB0, COM1, etc.)
PortType.BOOTSEL for RP2040 BOOTSEL upload via picotool
PortType.MQTT for MQTT logging
PortType.MQTTIP for MQTT IP lookup
PortType.NETWORK for IP addresses, hostnames, or mDNS names
"""
if port == "BOOTSEL":
return PortType.BOOTSEL
if port.startswith("/") or port.startswith("COM"):
return PortType.SERIAL
if port == "MQTT":
@@ -628,6 +690,47 @@ def _check_and_emit_build_info() -> None:
)
def _get_configured_xtal_freq() -> int | None:
"""Read the configured crystal frequency from the sdkconfig file."""
sdkconfig_path = CORE.relative_build_path(f"sdkconfig.{CORE.name}")
if not sdkconfig_path.is_file():
return None
with suppress(OSError, ValueError):
content = sdkconfig_path.read_text()
for line in content.splitlines():
if line.startswith("CONFIG_XTAL_FREQ="):
return int(line.split("=", 1)[1])
return None
def _make_crystal_freq_callback(
configured_freq: int,
) -> Callable[[str], str | None]:
"""Create a callback that checks esptool crystal frequency output."""
crystal_re = re.compile(r"Crystal frequency:\s+(\d+)\s*MHz")
def check_crystal_line(line: str) -> str | None:
if not (match := crystal_re.search(line)):
return None
detected = int(match.group(1))
if detected == configured_freq:
return None
return (
f"\n\033[33mWARNING: Crystal frequency mismatch! "
f"Device reports {detected}MHz but firmware is configured "
f"for {configured_freq}MHz.\n"
f"UART logging and other clock-dependent features will not "
f"work correctly.\n"
f"Set the correct crystal frequency with sdkconfig_options:\n"
f" esp32:\n"
f" framework:\n"
f" sdkconfig_options:\n"
f" CONFIG_XTAL_FREQ_{detected}: 'y'\033[0m\n\n"
)
return check_crystal_line
def upload_using_esptool(
config: ConfigType, port: str, file: str, speed: int
) -> str | int:
@@ -656,6 +759,14 @@ def upload_using_esptool(
mcu = get_esp32_variant().lower()
line_callbacks: list[Callable[[str], str | None]] = []
if (
CORE.is_esp32
and file is None
and (configured_freq := _get_configured_xtal_freq()) is not None
):
line_callbacks.append(_make_crystal_freq_callback(configured_freq))
def run_esptool(baud_rate):
cmd = [
"esptool",
@@ -680,9 +791,13 @@ def upload_using_esptool(
if os.environ.get("ESPHOME_USE_SUBPROCESS") is None:
import esptool
return run_external_command(esptool.main, *cmd) # pylint: disable=no-member
return run_external_command(
esptool.main, # pylint: disable=no-member
*cmd,
line_callbacks=line_callbacks,
)
return run_external_process(*cmd)
return run_external_process(*cmd, line_callbacks=line_callbacks)
rc = run_esptool(first_baudrate)
if rc == 0 or first_baudrate == 115200:
@@ -695,15 +810,140 @@ def upload_using_esptool(
return run_esptool(115200)
def upload_using_platformio(config: ConfigType, port: str):
def upload_using_platformio(config: ConfigType, port: str) -> int:
from esphome import platformio_api
# RP2040 platform-raspberrypi build recipe expects firmware.bin.signed for
# the upload target, but 'nobuild' skips the build phase that creates it.
# Create it here so the upload doesn't fail.
if CORE.data.get(KEY_CORE, {}).get(KEY_TARGET_PLATFORM) == PLATFORM_RP2040:
idedata = platformio_api.get_idedata(config)
build_dir = Path(idedata.firmware_elf_path).parent
firmware_bin = build_dir / "firmware.bin"
signed_bin = build_dir / "firmware.bin.signed"
if firmware_bin.is_file() and not signed_bin.is_file():
shutil.copy2(firmware_bin, signed_bin)
upload_args = ["-t", "upload", "-t", "nobuild"]
if port is not None:
upload_args += ["--upload-port", port]
return platformio_api.run_platformio_cli_run(config, CORE.verbose, *upload_args)
def _find_picotool() -> Path | None:
"""Find the picotool binary from PlatformIO packages."""
from esphome import platformio_api
try:
idedata = platformio_api.get_idedata(CORE.config)
except Exception: # noqa: BLE001 # pylint: disable=broad-except
return None
return get_picotool_path(idedata.cc_path)
def upload_using_picotool(config: ConfigType) -> int:
"""Upload firmware to RP2040 in BOOTSEL mode using picotool.
Uses picotool to load the ELF firmware directly via USB, avoiding
the mass storage copy approach that causes "disk not ejected properly"
warnings on macOS.
"""
from esphome import platformio_api
idedata = platformio_api.get_idedata(config)
firmware_elf = Path(idedata.firmware_elf_path)
if not firmware_elf.is_file():
_LOGGER.error(
"Firmware ELF file not found at %s. "
"Make sure the project has been compiled first.",
firmware_elf,
)
return 1
picotool = get_picotool_path(idedata.cc_path)
if picotool is None:
_LOGGER.error(
"picotool not found. Ensure the RP2040 PlatformIO platform "
"is installed (%s).",
PICOTOOL_PACKAGE,
)
return 1
_LOGGER.info("Uploading firmware to RP2040 via picotool...")
try:
# Don't capture stdout — let picotool write directly to the terminal
# so progress bars display in real-time with \r updates.
# Capture stderr only so we can detect permission errors.
result = subprocess.run(
[str(picotool), "load", "-v", "-x", str(firmware_elf)],
stderr=subprocess.PIPE,
timeout=60,
check=False,
)
except subprocess.TimeoutExpired:
_LOGGER.error("picotool upload timed out after 60 seconds.")
return 1
except OSError as err:
_LOGGER.error("Failed to run picotool: %s", err)
return 1
if result.returncode != 0:
stderr = result.stderr.decode("utf-8", errors="replace").strip()
if stderr:
for line in stderr.splitlines():
safe_print(line)
if is_picotool_usb_permission_error(stderr):
msg = "Permission denied accessing USB device."
if sys.platform.startswith("linux"):
msg += f" {_RP2040_UDEV_HINT}"
_LOGGER.error(msg)
else:
_LOGGER.error("picotool upload failed (exit code %d).", result.returncode)
return 1
return 0
def _wait_for_serial_port(
port: str | None = None,
timeout: float = 30.0,
known_ports: set[str] | None = None,
) -> None:
"""Wait for a serial port to appear, e.g. after a device reboot.
USB-CDC devices disappear briefly after flashing while the device
reboots and re-enumerates on the USB bus.
If port is given, wait for that specific path. If known_ports is
given, wait for a new port that wasn't in the set. Otherwise wait
for any serial port to appear.
"""
def _port_found() -> bool:
if port is not None:
if os.name == "posix":
return os.path.exists(port)
return any(p.path == port for p in get_serial_ports())
ports = get_serial_ports()
if known_ports is not None:
return any(p.path not in known_ports for p in ports)
return bool(ports)
if _port_found():
return
if port is not None:
_LOGGER.info("Waiting for %s to come online...", port)
else:
_LOGGER.info("Waiting for device to reboot...")
start = time.monotonic()
while time.monotonic() - start < timeout:
time.sleep(0.05)
if _port_found():
time.sleep(0.05)
return
def check_permissions(port: str):
if os.name == "posix" and get_port_type(port) == PortType.SERIAL:
# Check if we can open selected serial port
@@ -733,7 +973,15 @@ def upload_program(
except AttributeError:
pass
if get_port_type(host) == PortType.SERIAL:
port_type = get_port_type(host)
if port_type == PortType.BOOTSEL:
exit_code = upload_using_picotool(config)
# Return None for device - BOOTSEL can't be used for logging,
# so command_run will show the interactive chooser for log source
return exit_code, None
if port_type == PortType.SERIAL:
check_permissions(host)
exit_code = 1
@@ -787,6 +1035,7 @@ def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int
port_type = get_port_type(port)
if port_type == PortType.SERIAL:
_wait_for_serial_port(port)
check_permissions(port)
return run_miniterm(config, port, args)
@@ -925,6 +1174,9 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
purpose=Purpose.UPLOADING,
)
# Snapshot current serial ports before upload so we can detect new ones
pre_upload_ports = {p.path for p in get_serial_ports()}
exit_code, successful_device = upload_program(config, args, devices)
if exit_code == 0:
_LOGGER.info("Successfully uploaded program.")
@@ -935,6 +1187,19 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
if args.no_logs:
return 0
# After BOOTSEL upload, wait for a new serial port to appear
# so it shows up in the log chooser
if (
successful_device is None
and CORE.data.get(KEY_CORE, {}).get(KEY_TARGET_PLATFORM) == PLATFORM_RP2040
):
_wait_for_serial_port(known_ports=pre_upload_ports)
# If exactly one new serial port appeared, use it directly
serial_ports = get_serial_ports()
new_ports = [p for p in serial_ports if p.path not in pre_upload_ports]
if len(new_ports) == 1:
successful_device = new_ports[0].path
# For logs, prefer the device we successfully uploaded to
devices = choose_upload_log_host(
default=successful_device,
@@ -1044,9 +1309,8 @@ 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"]
dashboard = ["--dashboard"] if CORE.dashboard else []
return [*ESPHOME_COMMAND, *dashboard, "run", f, "--no-logs", "--device", "OTA"]
return run_multiple_configs(files, build_command)
@@ -1195,7 +1459,7 @@ def command_rename(args: ArgsProtocol, config: ConfigType) -> int | None:
new_path.write_text(new_raw, encoding="utf-8")
rc = run_external_process("esphome", "config", str(new_path))
rc = run_external_process(*ESPHOME_COMMAND, "config", str(new_path))
if rc != 0:
print(color(AnsiFore.BOLD_RED, "Rename failed. Reverting changes."))
new_path.unlink()
@@ -1213,7 +1477,7 @@ def command_rename(args: ArgsProtocol, config: ConfigType) -> int | None:
cli_args.insert(0, "--dashboard")
try:
rc = run_external_process("esphome", *cli_args)
rc = run_external_process(*ESPHOME_COMMAND, *cli_args)
except KeyboardInterrupt:
rc = 1
if rc != 0:
@@ -1610,7 +1874,7 @@ def run_esphome(argv):
# argv[0] is the program path, skip it since we prefix with "esphome"
def build_command(f):
return (
["esphome"]
[*ESPHOME_COMMAND]
+ [arg for arg in argv[1:] if arg not in args.configuration]
+ [str(f)]
)
+30 -8
View File
@@ -1,3 +1,5 @@
import logging
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import (
@@ -57,22 +59,41 @@ def maybe_conf(conf, *validators):
return validate
_LOGGER = logging.getLogger(__name__)
def register_action(
name: str,
action_type: MockObjClass,
schema: cv.Schema,
*,
synchronous: bool = False,
synchronous: bool | None = None,
):
"""Register an action type.
Actions default to ``synchronous=False`` (safe default), meaning string
arguments use owning std::string to prevent dangling references.
All callers must pass ``synchronous`` explicitly.
Set ``synchronous=True`` only for actions that complete synchronously
and never store trigger arguments for later execution. This allows
the code generator to use non-owning StringRef for zero-copy access.
``synchronous=True`` — the action never defers ``play_next_()`` to a
later point (callback, timer, or ``loop()``). Trigger arguments are
only used during the initial call, so string args can use non-owning
StringRef for zero-copy access.
``synchronous=False`` — the action defers ``play_next_()`` via a
callback, timer, or ``Component::loop()``. Trigger arguments must
outlive the initial call, so string args use owning std::string to
prevent dangling references.
"""
if synchronous is None:
_LOGGER.warning(
"register_action('%s', ...) is missing the synchronous= parameter. "
"Defaulting to synchronous=False (safe but prevents StringRef "
"optimization). Check the C++ class: use synchronous=False if "
"play_next_() is deferred to a callback, timer, or loop(); "
"use synchronous=True if play_next_() always runs before the "
"initial play/play_complex call returns",
name,
)
synchronous = False
return ACTION_REGISTRY.register(name, action_type, schema, synchronous=synchronous)
@@ -353,6 +374,7 @@ async def component_is_idle_condition_to_code(
"delay",
DelayAction,
cv.templatable(cv.positive_time_period_milliseconds),
synchronous=False,
)
async def delay_action_to_code(
config: ConfigType,
@@ -465,7 +487,7 @@ _validate_wait_until = cv.maybe_simple_value(
)
@register_action("wait_until", WaitUntilAction, _validate_wait_until)
@register_action("wait_until", WaitUntilAction, _validate_wait_until, synchronous=False)
async def wait_until_action_to_code(
config: ConfigType,
action_id: ID,
@@ -611,7 +633,7 @@ def has_non_synchronous_actions(actions: ConfigType) -> bool:
Non-synchronous actions (delay, wait_until, script.wait, etc.) store
trigger args for later execution, making non-owning types like StringRef
unsafe. Actions that haven't been audited default to non-synchronous.
unsafe.
"""
if isinstance(actions, list):
return any(has_non_synchronous_actions(item) for item in actions)
+3 -3
View File
@@ -72,7 +72,7 @@ def get_component_cmakelists(minimal: bool = False) -> str:
# 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 ""
compile_defs_str = "\n ".join(sorted(compile_defs)) if compile_defs else ""
# Extract compile options (-W flags, excluding linker flags)
compile_opts = [
@@ -80,11 +80,11 @@ def get_component_cmakelists(minimal: bool = False) -> str:
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 ""
compile_opts_str = "\n ".join(sorted(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 ""
link_opts_str = "\n ".join(sorted(link_opts)) if link_opts else ""
return f"""\
# Auto-generated by ESPHome
+2
View File
@@ -92,6 +92,7 @@ AGS10_NEW_I2C_ADDRESS_SCHEMA = cv.maybe_simple_value(
"ags10.new_i2c_address",
AGS10NewI2cAddressAction,
AGS10_NEW_I2C_ADDRESS_SCHEMA,
synchronous=True,
)
async def ags10newi2caddress_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
@@ -121,6 +122,7 @@ AGS10_SET_ZERO_POINT_SCHEMA = cv.Schema(
"ags10.set_zero_point",
AGS10SetZeroPointAction,
AGS10_SET_ZERO_POINT_SCHEMA,
synchronous=True,
)
async def ags10setzeropoint_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
+4 -1
View File
@@ -34,7 +34,10 @@ SET_AUTO_MUTE_ACTION_SCHEMA = cv.maybe_simple_value(
@automation.register_action(
"aic3204.set_auto_mute_mode", SetAutoMuteAction, SET_AUTO_MUTE_ACTION_SCHEMA
"aic3204.set_auto_mute_mode",
SetAutoMuteAction,
SET_AUTO_MUTE_ACTION_SCHEMA,
synchronous=True,
)
async def aic3204_set_volume_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -243,7 +243,10 @@ async def new_alarm_control_panel(config, *args):
@automation.register_action(
"alarm_control_panel.arm_away", ArmAwayAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
"alarm_control_panel.arm_away",
ArmAwayAction,
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
synchronous=True,
)
async def alarm_action_arm_away_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -255,7 +258,10 @@ async def alarm_action_arm_away_to_code(config, action_id, template_arg, args):
@automation.register_action(
"alarm_control_panel.arm_home", ArmHomeAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
"alarm_control_panel.arm_home",
ArmHomeAction,
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
synchronous=True,
)
async def alarm_action_arm_home_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -267,7 +273,10 @@ async def alarm_action_arm_home_to_code(config, action_id, template_arg, args):
@automation.register_action(
"alarm_control_panel.arm_night", ArmNightAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
"alarm_control_panel.arm_night",
ArmNightAction,
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
synchronous=True,
)
async def alarm_action_arm_night_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -279,7 +288,10 @@ async def alarm_action_arm_night_to_code(config, action_id, template_arg, args):
@automation.register_action(
"alarm_control_panel.disarm", DisarmAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
"alarm_control_panel.disarm",
DisarmAction,
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
synchronous=True,
)
async def alarm_action_disarm_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -291,7 +303,10 @@ async def alarm_action_disarm_to_code(config, action_id, template_arg, args):
@automation.register_action(
"alarm_control_panel.pending", PendingAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
"alarm_control_panel.pending",
PendingAction,
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
synchronous=True,
)
async def alarm_action_pending_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -299,7 +314,10 @@ async def alarm_action_pending_to_code(config, action_id, template_arg, args):
@automation.register_action(
"alarm_control_panel.triggered", TriggeredAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
"alarm_control_panel.triggered",
TriggeredAction,
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
synchronous=True,
)
async def alarm_action_trigger_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -307,7 +325,10 @@ async def alarm_action_trigger_to_code(config, action_id, template_arg, args):
@automation.register_action(
"alarm_control_panel.chime", ChimeAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
"alarm_control_panel.chime",
ChimeAction,
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
synchronous=True,
)
async def alarm_action_chime_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -315,7 +336,10 @@ async def alarm_action_chime_to_code(config, action_id, template_arg, args):
@automation.register_action(
"alarm_control_panel.ready", ReadyAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
"alarm_control_panel.ready",
ReadyAction,
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
synchronous=True,
)
@automation.register_condition(
"alarm_control_panel.ready",
+9 -3
View File
@@ -69,9 +69,15 @@ SET_FRAME_SCHEMA = cv.Schema(
)
@automation.register_action("animation.next_frame", NextFrameAction, NEXT_FRAME_SCHEMA)
@automation.register_action("animation.prev_frame", PrevFrameAction, PREV_FRAME_SCHEMA)
@automation.register_action("animation.set_frame", SetFrameAction, SET_FRAME_SCHEMA)
@automation.register_action(
"animation.next_frame", NextFrameAction, NEXT_FRAME_SCHEMA, synchronous=True
)
@automation.register_action(
"animation.prev_frame", PrevFrameAction, PREV_FRAME_SCHEMA, synchronous=True
)
@automation.register_action(
"animation.set_frame", SetFrameAction, SET_FRAME_SCHEMA, synchronous=True
)
async def animation_action_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
+1
View File
@@ -712,6 +712,7 @@ API_RESPOND_ACTION_SCHEMA = cv.All(
"api.respond",
APIRespondAction,
API_RESPOND_ACTION_SCHEMA,
synchronous=True,
)
async def api_respond_to_code(
config: ConfigType,
+13
View File
@@ -0,0 +1,13 @@
#include "api_buffer.h"
namespace esphome::api {
void APIBuffer::grow_(size_t n) {
auto new_data = make_buffer(n);
if (this->size_)
std::memcpy(new_data.get(), this->data_.get(), this->size_);
this->data_ = std::move(new_data);
this->capacity_ = n;
}
} // namespace esphome::api
+67
View File
@@ -0,0 +1,67 @@
#pragma once
#include <cstdint>
#include <cstring>
#include <memory>
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
namespace esphome::api {
/// Helper to use make_unique_for_overwrite where available (skips zero-fill),
/// falling back to make_unique on older GCC (ESP8266, LibreTiny).
inline std::unique_ptr<uint8_t[]> make_buffer(size_t n) {
#if defined(USE_ESP8266) || defined(USE_LIBRETINY)
return std::make_unique<uint8_t[]>(n);
#else
return std::make_unique_for_overwrite<uint8_t[]>(n);
#endif
}
/// Byte buffer that skips zero-initialization on resize().
///
/// std::vector<uint8_t>::resize() zero-fills new bytes via memset. For the
/// shared protobuf write buffer, every byte is overwritten by the encoder,
/// making the zero-fill pure waste. For the receive buffer, bytes are
/// overwritten by socket reads.
///
/// Designed for bulk clear/resize/overwrite patterns. grow_() allocates
/// exactly the requested size (no growth factor) since callers resize to
/// known sizes rather than appending incrementally.
///
/// Safe because: callers always write exactly the number of bytes they
/// resize for. In the protobuf write path, debug_check_bounds_ validates
/// writes in debug builds.
class APIBuffer {
public:
void clear() { this->size_ = 0; }
inline void reserve(size_t n) ESPHOME_ALWAYS_INLINE {
if (n > this->capacity_)
this->grow_(n);
}
inline void resize(size_t n) ESPHOME_ALWAYS_INLINE {
this->reserve(n);
this->size_ = n; // no zero-fill
}
uint8_t *data() { return this->data_.get(); }
const uint8_t *data() const { return this->data_.get(); }
size_t size() const { return this->size_; }
bool empty() const { return this->size_ == 0; }
uint8_t &operator[](size_t i) { return this->data_[i]; }
const uint8_t &operator[](size_t i) const { return this->data_[i]; }
/// Release all memory (equivalent to std::vector swap trick).
void release() {
this->data_.reset();
this->size_ = 0;
this->capacity_ = 0;
}
protected:
void grow_(size_t n);
std::unique_ptr<uint8_t[]> data_;
size_t size_{0};
size_t capacity_{0};
};
} // namespace esphome::api
+2 -2
View File
@@ -2016,7 +2016,7 @@ uint16_t APIConnection::encode_to_buffer(uint32_t calculated_size, MessageEncode
if (total_calculated_size > remaining_size)
return 0; // Doesn't fit
std::vector<uint8_t> &shared_buf = conn->parent_->get_shared_buffer_ref();
auto &shared_buf = conn->parent_->get_shared_buffer_ref();
if (conn->flags_.batch_first_message) {
// First message - buffer already prepared by caller, just clear flag
@@ -2184,7 +2184,7 @@ void APIConnection::process_batch_() {
// Separated from process_batch_() so the single-message fast path gets a minimal
// stack frame without the MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo) array.
void APIConnection::process_batch_multi_(std::vector<uint8_t> &shared_buf, size_t num_items, uint8_t header_padding,
void APIConnection::process_batch_multi_(APIBuffer &shared_buf, size_t num_items, uint8_t header_padding,
uint8_t footer_size) {
// Ensure MessageInfo remains trivially destructible for our placement new approach
static_assert(std::is_trivially_destructible<MessageInfo>::value,
+16 -4
View File
@@ -14,6 +14,12 @@
#include "api_server.h"
#include "esphome/core/application.h"
#include "esphome/core/component.h"
#ifdef USE_ESP32_CRASH_HANDLER
#include "esphome/components/esp32/crash_handler.h"
#endif
#ifdef USE_RP2040_CRASH_HANDLER
#include "esphome/components/rp2040/crash_handler.h"
#endif
#include "esphome/core/entity_base.h"
#include "esphome/core/string_ref.h"
@@ -235,6 +241,12 @@ class APIConnection final : public APIServerConnectionBase {
this->flags_.log_subscription = msg.level;
if (msg.dump_config)
App.schedule_dump_config();
#ifdef USE_ESP32_CRASH_HANDLER
esp32::crash_handler_log();
#endif
#ifdef USE_RP2040_CRASH_HANDLER
rp2040::crash_handler_log();
#endif
}
#ifdef USE_API_HOMEASSISTANT_SERVICES
void on_subscribe_homeassistant_services_request() override { this->flags_.service_call_subscription = true; }
@@ -288,7 +300,7 @@ class APIConnection final : public APIServerConnectionBase {
}
}
void prepare_first_message_buffer(std::vector<uint8_t> &shared_buf, size_t header_padding, size_t total_size) {
void prepare_first_message_buffer(APIBuffer &shared_buf, size_t header_padding, size_t total_size) {
shared_buf.clear();
// Reserve space for header padding + message + footer
// - Header padding: space for protocol headers (7 bytes for Noise, 6 for Plaintext)
@@ -299,7 +311,7 @@ class APIConnection final : public APIServerConnectionBase {
}
// Convenience overload - computes frame overhead internally
void prepare_first_message_buffer(std::vector<uint8_t> &shared_buf, size_t payload_size) {
void prepare_first_message_buffer(APIBuffer &shared_buf, size_t payload_size) {
const uint8_t header_padding = this->helper_->frame_header_padding();
const uint8_t footer_size = this->helper_->frame_footer_size();
this->prepare_first_message_buffer(shared_buf, header_padding, payload_size + header_padding + footer_size);
@@ -687,8 +699,8 @@ class APIConnection final : public APIServerConnectionBase {
bool schedule_batch_();
void process_batch_();
void process_batch_multi_(std::vector<uint8_t> &shared_buf, size_t num_items, uint8_t header_padding,
uint8_t footer_size) __attribute__((noinline));
void process_batch_multi_(APIBuffer &shared_buf, size_t num_items, uint8_t header_padding, uint8_t footer_size)
__attribute__((noinline));
void clear_batch_() {
this->deferred_batch_.clear();
this->flags_.batch_scheduled = false;
+3 -7
View File
@@ -5,10 +5,10 @@
#include <memory>
#include <span>
#include <utility>
#include <vector>
#include "esphome/core/defines.h"
#ifdef USE_API
#include "esphome/components/api/api_buffer.h"
#include "esphome/components/socket/socket.h"
#include "esphome/core/application.h"
#include "esphome/core/log.h"
@@ -178,8 +178,7 @@ class APIFrameHelper {
// rx_buf_len_ tracks bytes read so far; if non-zero, we're mid-frame
// and clearing would lose partially received data.
if (this->rx_buf_len_ == 0) {
// Use swap trick since shrink_to_fit() is non-binding and may be ignored
std::vector<uint8_t>().swap(this->rx_buf_);
this->rx_buf_.release();
}
}
@@ -206,9 +205,6 @@ class APIFrameHelper {
// Common socket write error handling
APIError handle_socket_write_error_();
template<typename StateEnum>
APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf,
const std::string &info, StateEnum &state, StateEnum failed_state);
// Socket ownership (4 bytes on 32-bit, 8 bytes on 64-bit)
std::unique_ptr<socket::Socket> socket_;
@@ -245,7 +241,7 @@ class APIFrameHelper {
// Containers (size varies, but typically 12+ bytes on 32-bit)
std::array<std::unique_ptr<SendBuffer>, API_MAX_SEND_QUEUE> tx_buf_;
std::vector<uint8_t> rx_buf_;
APIBuffer rx_buf_;
// Client name buffer - stores name from Hello message or initial peername
char client_name_[CLIENT_INFO_NAME_MAX_LEN]{};
@@ -207,9 +207,7 @@ APIError APINoiseFrameHelper::try_read_frame_() {
// During handshake, rx_buf_.size() is used in prologue construction, so
// the buffer must be exactly msg_size to avoid prologue mismatch.)
uint16_t alloc_size = msg_size + (is_data ? RX_BUF_NULL_TERMINATOR : 0);
if (this->rx_buf_.size() != alloc_size) {
this->rx_buf_.resize(alloc_size);
}
this->rx_buf_.resize(alloc_size);
if (rx_buf_len_ < msg_size) {
// more data to read
@@ -571,8 +569,7 @@ APIError APINoiseFrameHelper::init_handshake_() {
if (aerr != APIError::OK)
return aerr;
// set_prologue copies it into handshakestate, so we can get rid of it now
// Use swap idiom to actually release memory (= {} only clears size, not capacity)
std::vector<uint8_t>().swap(prologue_);
prologue_.release();
err = noise_handshakestate_start(handshake_);
aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_start"), APIError::HANDSHAKESTATE_SETUP_FAILED);
@@ -43,8 +43,8 @@ class APINoiseFrameHelper final : public APIFrameHelper {
// Reference to noise context (4 bytes on 32-bit)
APINoiseContext &ctx_;
// Vector (12 bytes on 32-bit)
std::vector<uint8_t> prologue_;
// Buffer for noise handshake prologue (released after handshake)
APIBuffer prologue_;
// NoiseProtocolId (size depends on implementation)
NoiseProtocolId nid_;
@@ -128,46 +128,44 @@ APIError APIPlaintextFrameHelper::try_read_frame_() {
// Skip indicator byte at position 0
uint8_t varint_pos = 1;
uint32_t consumed = 0;
auto msg_size_varint = ProtoVarInt::parse(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos, &consumed);
// rx_header_buf_pos_ >= 3 and varint_pos == 1, so len >= 2
auto msg_size_varint = ProtoVarInt::parse_non_empty(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos);
if (!msg_size_varint.has_value()) {
// not enough data there yet
continue;
}
if (msg_size_varint->as_uint32() > MAX_MESSAGE_SIZE) {
if (msg_size_varint.value > MAX_MESSAGE_SIZE) {
state_ = State::FAILED;
HELPER_LOG("Bad packet: message size %" PRIu32 " exceeds maximum %u", msg_size_varint->as_uint32(),
MAX_MESSAGE_SIZE);
HELPER_LOG("Bad packet: message size %" PRIu32 " exceeds maximum %u",
static_cast<uint32_t>(msg_size_varint.value), MAX_MESSAGE_SIZE);
return APIError::BAD_DATA_PACKET;
}
rx_header_parsed_len_ = msg_size_varint->as_uint16();
rx_header_parsed_len_ = static_cast<uint16_t>(msg_size_varint.value);
// Move to next varint position
varint_pos += consumed;
varint_pos += msg_size_varint.consumed;
auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos, &consumed);
auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos);
if (!msg_type_varint.has_value()) {
// not enough data there yet
continue;
}
if (msg_type_varint->as_uint32() > std::numeric_limits<uint16_t>::max()) {
if (msg_type_varint.value > std::numeric_limits<uint16_t>::max()) {
state_ = State::FAILED;
HELPER_LOG("Bad packet: message type %" PRIu32 " exceeds maximum %u", msg_type_varint->as_uint32(),
std::numeric_limits<uint16_t>::max());
HELPER_LOG("Bad packet: message type %" PRIu32 " exceeds maximum %u",
static_cast<uint32_t>(msg_type_varint.value), std::numeric_limits<uint16_t>::max());
return APIError::BAD_DATA_PACKET;
}
rx_header_parsed_type_ = msg_type_varint->as_uint16();
rx_header_parsed_type_ = static_cast<uint16_t>(msg_type_varint.value);
rx_header_parsed_ = true;
}
// header reading done
// Reserve space for body (+ null terminator so protobuf StringRef fields
// can be safely null-terminated in-place after decode)
if (this->rx_buf_.size() != this->rx_header_parsed_len_ + RX_BUF_NULL_TERMINATOR) {
this->rx_buf_.resize(this->rx_header_parsed_len_ + RX_BUF_NULL_TERMINATOR);
}
this->rx_buf_.resize(this->rx_header_parsed_len_ + RX_BUF_NULL_TERMINATOR);
if (rx_buf_len_ < rx_header_parsed_len_) {
// more data to read
File diff suppressed because it is too large Load Diff
+51 -51
View File
@@ -399,7 +399,7 @@ class HelloRequest final : public ProtoDecodableMessage {
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
class HelloResponse final : public ProtoMessage {
public:
@@ -688,7 +688,7 @@ class CoverCommandRequest final : public CommandProtoMessage {
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
#endif
#ifdef USE_FAN
@@ -756,7 +756,7 @@ class FanCommandRequest final : public CommandProtoMessage {
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
#endif
#ifdef USE_LIGHT
@@ -846,7 +846,7 @@ class LightCommandRequest final : public CommandProtoMessage {
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
#endif
#ifdef USE_SENSOR
@@ -936,7 +936,7 @@ class SwitchCommandRequest final : public CommandProtoMessage {
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
#endif
#ifdef USE_TEXT_SENSOR
@@ -988,7 +988,7 @@ class SubscribeLogsRequest final : public ProtoDecodableMessage {
#endif
protected:
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
class SubscribeLogsResponse final : public ProtoMessage {
public:
@@ -1110,7 +1110,7 @@ class HomeassistantActionResponse final : public ProtoDecodableMessage {
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
@@ -1176,7 +1176,7 @@ class DSTRule final : public ProtoDecodableMessage {
#endif
protected:
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
class ParsedTimezone final : public ProtoDecodableMessage {
public:
@@ -1190,7 +1190,7 @@ class ParsedTimezone final : public ProtoDecodableMessage {
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
class GetTimeResponse final : public ProtoDecodableMessage {
public:
@@ -1261,7 +1261,7 @@ class ExecuteServiceArgument final : public ProtoDecodableMessage {
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
class ExecuteServiceRequest final : public ProtoDecodableMessage {
public:
@@ -1286,7 +1286,7 @@ class ExecuteServiceRequest final : public ProtoDecodableMessage {
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
#endif
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
@@ -1365,7 +1365,7 @@ class CameraImageRequest final : public ProtoDecodableMessage {
#endif
protected:
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
#endif
#ifdef USE_CLIMATE
@@ -1464,7 +1464,7 @@ class ClimateCommandRequest final : public CommandProtoMessage {
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
#endif
#ifdef USE_WATER_HEATER
@@ -1528,7 +1528,7 @@ class WaterHeaterCommandRequest final : public CommandProtoMessage {
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
#endif
#ifdef USE_NUMBER
@@ -1584,7 +1584,7 @@ class NumberCommandRequest final : public CommandProtoMessage {
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
#endif
#ifdef USE_SELECT
@@ -1636,7 +1636,7 @@ class SelectCommandRequest final : public CommandProtoMessage {
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
#endif
#ifdef USE_SIREN
@@ -1696,7 +1696,7 @@ class SirenCommandRequest final : public CommandProtoMessage {
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
#endif
#ifdef USE_LOCK
@@ -1752,7 +1752,7 @@ class LockCommandRequest final : public CommandProtoMessage {
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
#endif
#ifdef USE_BUTTON
@@ -1785,7 +1785,7 @@ class ButtonCommandRequest final : public CommandProtoMessage {
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
#endif
#ifdef USE_MEDIA_PLAYER
@@ -1862,7 +1862,7 @@ class MediaPlayerCommandRequest final : public CommandProtoMessage {
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
#endif
#ifdef USE_BLUETOOTH_PROXY
@@ -1879,7 +1879,7 @@ class SubscribeBluetoothLEAdvertisementsRequest final : public ProtoDecodableMes
#endif
protected:
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
class BluetoothLERawAdvertisement final : public ProtoMessage {
public:
@@ -1929,7 +1929,7 @@ class BluetoothDeviceRequest final : public ProtoDecodableMessage {
#endif
protected:
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
class BluetoothDeviceConnectionResponse final : public ProtoMessage {
public:
@@ -1963,7 +1963,7 @@ class BluetoothGATTGetServicesRequest final : public ProtoDecodableMessage {
#endif
protected:
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
class BluetoothGATTDescriptor final : public ProtoMessage {
public:
@@ -2054,7 +2054,7 @@ class BluetoothGATTReadRequest final : public ProtoDecodableMessage {
#endif
protected:
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
class BluetoothGATTReadResponse final : public ProtoMessage {
public:
@@ -2097,7 +2097,7 @@ class BluetoothGATTWriteRequest final : public ProtoDecodableMessage {
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
class BluetoothGATTReadDescriptorRequest final : public ProtoDecodableMessage {
public:
@@ -2113,7 +2113,7 @@ class BluetoothGATTReadDescriptorRequest final : public ProtoDecodableMessage {
#endif
protected:
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
class BluetoothGATTWriteDescriptorRequest final : public ProtoDecodableMessage {
public:
@@ -2132,7 +2132,7 @@ class BluetoothGATTWriteDescriptorRequest final : public ProtoDecodableMessage {
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
class BluetoothGATTNotifyRequest final : public ProtoDecodableMessage {
public:
@@ -2149,7 +2149,7 @@ class BluetoothGATTNotifyRequest final : public ProtoDecodableMessage {
#endif
protected:
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
class BluetoothGATTNotifyDataResponse final : public ProtoMessage {
public:
@@ -2329,7 +2329,7 @@ class BluetoothScannerSetModeRequest final : public ProtoDecodableMessage {
#endif
protected:
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
#endif
#ifdef USE_VOICE_ASSISTANT
@@ -2347,7 +2347,7 @@ class SubscribeVoiceAssistantRequest final : public ProtoDecodableMessage {
#endif
protected:
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
class VoiceAssistantAudioSettings final : public ProtoMessage {
public:
@@ -2396,7 +2396,7 @@ class VoiceAssistantResponse final : public ProtoDecodableMessage {
#endif
protected:
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
class VoiceAssistantEventData final : public ProtoDecodableMessage {
public:
@@ -2424,7 +2424,7 @@ class VoiceAssistantEventResponse final : public ProtoDecodableMessage {
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
class VoiceAssistantAudio final : public ProtoDecodableMessage {
public:
@@ -2444,7 +2444,7 @@ class VoiceAssistantAudio final : public ProtoDecodableMessage {
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
class VoiceAssistantTimerEventResponse final : public ProtoDecodableMessage {
public:
@@ -2465,7 +2465,7 @@ class VoiceAssistantTimerEventResponse final : public ProtoDecodableMessage {
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
class VoiceAssistantAnnounceRequest final : public ProtoDecodableMessage {
public:
@@ -2484,7 +2484,7 @@ class VoiceAssistantAnnounceRequest final : public ProtoDecodableMessage {
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
class VoiceAssistantAnnounceFinished final : public ProtoMessage {
public:
@@ -2530,7 +2530,7 @@ class VoiceAssistantExternalWakeWord final : public ProtoDecodableMessage {
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
class VoiceAssistantConfigurationRequest final : public ProtoDecodableMessage {
public:
@@ -2632,7 +2632,7 @@ class AlarmControlPanelCommandRequest final : public CommandProtoMessage {
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
#endif
#ifdef USE_TEXT
@@ -2687,7 +2687,7 @@ class TextCommandRequest final : public CommandProtoMessage {
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
#endif
#ifdef USE_DATETIME_DATE
@@ -2741,7 +2741,7 @@ class DateCommandRequest final : public CommandProtoMessage {
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
#endif
#ifdef USE_DATETIME_TIME
@@ -2795,7 +2795,7 @@ class TimeCommandRequest final : public CommandProtoMessage {
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
#endif
#ifdef USE_EVENT
@@ -2886,7 +2886,7 @@ class ValveCommandRequest final : public CommandProtoMessage {
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
#endif
#ifdef USE_DATETIME_DATETIME
@@ -2936,7 +2936,7 @@ class DateTimeCommandRequest final : public CommandProtoMessage {
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
#endif
#ifdef USE_UPDATE
@@ -2994,7 +2994,7 @@ class UpdateCommandRequest final : public CommandProtoMessage {
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
#endif
#ifdef USE_ZWAVE_PROXY
@@ -3034,7 +3034,7 @@ class ZWaveProxyRequest final : public ProtoDecodableMessage {
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
#endif
#ifdef USE_INFRARED
@@ -3079,7 +3079,7 @@ class InfraredRFTransmitRawTimingsRequest final : public ProtoDecodableMessage {
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
class InfraredRFReceiveEvent final : public ProtoMessage {
public:
@@ -3121,7 +3121,7 @@ class SerialProxyConfigureRequest final : public ProtoDecodableMessage {
#endif
protected:
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
class SerialProxyDataReceived final : public ProtoMessage {
public:
@@ -3161,7 +3161,7 @@ class SerialProxyWriteRequest final : public ProtoDecodableMessage {
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
class SerialProxySetModemPinsRequest final : public ProtoDecodableMessage {
public:
@@ -3177,7 +3177,7 @@ class SerialProxySetModemPinsRequest final : public ProtoDecodableMessage {
#endif
protected:
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
class SerialProxyGetModemPinsRequest final : public ProtoDecodableMessage {
public:
@@ -3192,7 +3192,7 @@ class SerialProxyGetModemPinsRequest final : public ProtoDecodableMessage {
#endif
protected:
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
class SerialProxyGetModemPinsResponse final : public ProtoMessage {
public:
@@ -3225,7 +3225,7 @@ class SerialProxyRequest final : public ProtoDecodableMessage {
#endif
protected:
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
class SerialProxyRequestResponse final : public ProtoMessage {
public:
@@ -3265,7 +3265,7 @@ class BluetoothSetConnectionParamsRequest final : public ProtoDecodableMessage {
#endif
protected:
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
class BluetoothSetConnectionParamsResponse final : public ProtoMessage {
public:
+3 -2
View File
@@ -2,6 +2,7 @@
#include "esphome/core/defines.h"
#ifdef USE_API
#include "api_buffer.h"
#include "api_noise_context.h"
#include "api_pb2.h"
#include "api_pb2_service.h"
@@ -65,7 +66,7 @@ class APIServer : public Component,
void set_max_connections(uint8_t max_connections) { this->max_connections_ = max_connections; }
// Get reference to shared buffer for API connections
std::vector<uint8_t> &get_shared_buffer_ref() { return shared_write_buffer_; }
APIBuffer &get_shared_buffer_ref() { return shared_write_buffer_; }
#ifdef USE_API_NOISE
bool save_noise_psk(psk_t psk, bool make_active = true);
@@ -276,7 +277,7 @@ class APIServer : public Component,
// Not pre-allocated: all send paths call prepare_first_message_buffer() which
// reserves the exact needed size. Pre-allocating here would cause heap fragmentation
// since the buffer would almost always reallocate on first use.
std::vector<uint8_t> shared_write_buffer_;
APIBuffer shared_write_buffer_;
#ifdef USE_API_HOMEASSISTANT_STATES
std::vector<HomeAssistantStateSubscription> state_subs_;
#endif
+21
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
import asyncio
from datetime import datetime
import importlib
import logging
from typing import TYPE_CHECKING, Any
import warnings
@@ -18,6 +19,7 @@ import contextlib
from esphome.const import CONF_KEY, CONF_PORT, __version__
from esphome.core import CORE
from esphome.platformio_api import process_stacktrace
from . import CONF_ENCRYPTION
@@ -55,9 +57,19 @@ async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None:
addresses=addresses, # Pass all addresses for automatic retry
)
dashboard = CORE.dashboard
backtrace_state = False
# Try platform-specific stacktrace handler first, fall back to generic
platform_process_stacktrace = None
try:
module = importlib.import_module("esphome.components." + CORE.target_platform)
platform_process_stacktrace = getattr(module, "process_stacktrace")
except (AttributeError, ImportError):
pass
def on_log(msg: SubscribeLogsResponse) -> None:
"""Handle a new log message."""
nonlocal backtrace_state
time_ = datetime.now()
message: bytes = msg.message
text = message.decode("utf8", "backslashreplace")
@@ -67,6 +79,15 @@ async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None:
)
for parsed_msg in parse_log_message(text, timestamp):
print(parsed_msg.replace("\033", "\\033") if dashboard else parsed_msg)
for raw_line in text.splitlines():
if platform_process_stacktrace:
backtrace_state = platform_process_stacktrace(
config, raw_line, backtrace_state
)
else:
backtrace_state = process_stacktrace(
config, raw_line, backtrace_state=backtrace_state
)
stop = await async_run(cli, on_log, name=name)
try:
+46 -29
View File
@@ -20,20 +20,40 @@ void ProtoWriteBuffer::encode_varint_raw_slow_(uint32_t value) {
*this->pos_++ = static_cast<uint8_t>(value);
}
ProtoVarIntResult ProtoVarInt::parse_slow(const uint8_t *buffer, uint32_t len) {
// Multi-byte varint: first byte already checked to have high bit set
uint32_t result32 = buffer[0] & 0x7F;
#ifdef USE_API_VARINT64
optional<ProtoVarInt> ProtoVarInt::parse_wide(const uint8_t *buffer, uint32_t len, uint32_t *consumed,
uint32_t result32) {
uint32_t limit = std::min(len, uint32_t(4));
#else
uint32_t limit = std::min(len, uint32_t(5));
#endif
for (uint32_t i = 1; i < limit; i++) {
uint8_t val = buffer[i];
result32 |= uint32_t(val & 0x7F) << (i * 7);
if ((val & 0x80) == 0) {
return {result32, i + 1};
}
}
#ifdef USE_API_VARINT64
return parse_wide(buffer, len, result32);
#else
return {0, PROTO_VARINT_PARSE_FAILED};
#endif
}
#ifdef USE_API_VARINT64
ProtoVarIntResult ProtoVarInt::parse_wide(const uint8_t *buffer, uint32_t len, uint32_t result32) {
uint64_t result64 = result32;
uint32_t limit = std::min(len, uint32_t(10));
for (uint32_t i = 4; i < limit; i++) {
uint8_t val = buffer[i];
result64 |= uint64_t(val & 0x7F) << (i * 7);
if ((val & 0x80) == 0) {
*consumed = i + 1;
return ProtoVarInt(result64);
return {result64, i + 1};
}
}
return {};
return {0, PROTO_VARINT_PARSE_FAILED};
}
#endif
@@ -43,18 +63,16 @@ uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size
const uint8_t *end = buffer + length;
while (ptr < end) {
uint32_t consumed;
// Parse field header (tag)
auto res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
// Parse field header (tag) - ptr < end guarantees len >= 1
auto res = ProtoVarInt::parse_non_empty(ptr, end - ptr);
if (!res.has_value()) {
break; // Invalid data, stop counting
}
uint32_t tag = res->as_uint32();
uint32_t tag = static_cast<uint32_t>(res.value);
uint32_t field_type = tag & WIRE_TYPE_MASK;
uint32_t field_id = tag >> 3;
ptr += consumed;
ptr += res.consumed;
// Count if this is the target field
if (field_id == target_field_id) {
@@ -64,20 +82,20 @@ uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size
// Skip field data based on wire type
switch (field_type) {
case WIRE_TYPE_VARINT: { // VarInt - parse and skip
res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
res = ProtoVarInt::parse(ptr, end - ptr);
if (!res.has_value()) {
return count; // Invalid data, return what we have
}
ptr += consumed;
ptr += res.consumed;
break;
}
case WIRE_TYPE_LENGTH_DELIMITED: { // Length-delimited - parse length and skip data
res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
res = ProtoVarInt::parse(ptr, end - ptr);
if (!res.has_value()) {
return count;
}
uint32_t field_length = res->as_uint32();
ptr += consumed;
uint32_t field_length = static_cast<uint32_t>(res.value);
ptr += res.consumed;
if (field_length > static_cast<size_t>(end - ptr)) {
return count; // Out of bounds
}
@@ -190,41 +208,40 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
const uint8_t *end = buffer + length;
while (ptr < end) {
uint32_t consumed;
// Parse field header
auto res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
// Parse field header - ptr < end guarantees len >= 1
auto res = ProtoVarInt::parse_non_empty(ptr, end - ptr);
if (!res.has_value()) {
ESP_LOGV(TAG, "Invalid field start at offset %ld", (long) (ptr - buffer));
return;
}
uint32_t tag = res->as_uint32();
uint32_t tag = static_cast<uint32_t>(res.value);
uint32_t field_type = tag & WIRE_TYPE_MASK;
uint32_t field_id = tag >> 3;
ptr += consumed;
ptr += res.consumed;
switch (field_type) {
case WIRE_TYPE_VARINT: { // VarInt
res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
res = ProtoVarInt::parse(ptr, end - ptr);
if (!res.has_value()) {
ESP_LOGV(TAG, "Invalid VarInt at offset %ld", (long) (ptr - buffer));
return;
}
if (!this->decode_varint(field_id, *res)) {
ESP_LOGV(TAG, "Cannot decode VarInt field %" PRIu32 " with value %" PRIu32 "!", field_id, res->as_uint32());
if (!this->decode_varint(field_id, res.value)) {
ESP_LOGV(TAG, "Cannot decode VarInt field %" PRIu32 " with value %" PRIu64 "!", field_id,
static_cast<uint64_t>(res.value));
}
ptr += consumed;
ptr += res.consumed;
break;
}
case WIRE_TYPE_LENGTH_DELIMITED: { // Length-delimited
res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
res = ProtoVarInt::parse(ptr, end - ptr);
if (!res.has_value()) {
ESP_LOGV(TAG, "Invalid Length Delimited at offset %ld", (long) (ptr - buffer));
return;
}
uint32_t field_length = res->as_uint32();
ptr += consumed;
uint32_t field_length = static_cast<uint32_t>(res.value);
ptr += res.consumed;
if (field_length > static_cast<size_t>(end - ptr)) {
ESP_LOGV(TAG, "Out-of-bounds Length Delimited at offset %ld", (long) (ptr - buffer));
return;
+47 -81
View File
@@ -1,6 +1,7 @@
#pragma once
#include "api_pb2_defines.h"
#include "api_buffer.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -98,90 +99,56 @@ inline void encode_varint_to_buffer(uint32_t val, uint8_t *buffer) {
* within the same function scope where temporaries are created.
*/
/// Representation of a VarInt - in ProtoBuf should be 64bit but we only use 32bit
/// Type used for decoded varint values - uint64_t when BLE needs 64-bit addresses, uint32_t otherwise
#ifdef USE_API_VARINT64
using proto_varint_value_t = uint64_t;
#else
using proto_varint_value_t = uint32_t;
#endif
/// Sentinel value for consumed field indicating parse failure
inline constexpr uint32_t PROTO_VARINT_PARSE_FAILED = 0;
/// Result of parsing a varint: value + number of bytes consumed.
/// consumed == PROTO_VARINT_PARSE_FAILED indicates parse failure (not enough data or invalid).
struct ProtoVarIntResult {
proto_varint_value_t value;
uint32_t consumed; // PROTO_VARINT_PARSE_FAILED = parse failed
constexpr bool has_value() const { return this->consumed != PROTO_VARINT_PARSE_FAILED; }
};
/// Static varint parsing methods for the protobuf wire format.
class ProtoVarInt {
public:
ProtoVarInt() : value_(0) {}
explicit ProtoVarInt(uint64_t value) : value_(value) {}
/// Parse a varint from buffer. consumed must be a valid pointer (not null).
static optional<ProtoVarInt> parse(const uint8_t *buffer, uint32_t len, uint32_t *consumed) {
/// Parse a varint from buffer. Caller must ensure len >= 1.
/// Returns result with consumed=0 on failure (truncated multi-byte varint).
static inline ProtoVarIntResult ESPHOME_ALWAYS_INLINE parse_non_empty(const uint8_t *buffer, uint32_t len) {
#ifdef ESPHOME_DEBUG_API
assert(consumed != nullptr);
assert(len > 0);
#endif
if (len == 0)
return {};
// Fast path: single-byte varints (0-127) are the most common case
// (booleans, small enums, field tags). Avoid loop overhead entirely.
if ((buffer[0] & 0x80) == 0) {
*consumed = 1;
return ProtoVarInt(buffer[0]);
}
// 32-bit phase: process remaining bytes with native 32-bit shifts.
// Without USE_API_VARINT64: cover bytes 1-4 (shifts 7, 14, 21, 28) — the uint32_t
// shift at byte 4 (shift by 28) may lose bits 32-34, but those are always zero for valid uint32 values.
// With USE_API_VARINT64: cover bytes 1-3 (shifts 7, 14, 21) so parse_wide handles
// byte 4+ with full 64-bit arithmetic (avoids truncating values > UINT32_MAX).
uint32_t result32 = buffer[0] & 0x7F;
#ifdef USE_API_VARINT64
uint32_t limit = std::min(len, uint32_t(4));
#else
uint32_t limit = std::min(len, uint32_t(5));
#endif
for (uint32_t i = 1; i < limit; i++) {
uint8_t val = buffer[i];
result32 |= uint32_t(val & 0x7F) << (i * 7);
if ((val & 0x80) == 0) {
*consumed = i + 1;
return ProtoVarInt(result32);
}
}
// 64-bit phase for remaining bytes (BLE addresses etc.)
#ifdef USE_API_VARINT64
return parse_wide(buffer, len, consumed, result32);
#else
return {};
#endif
// (booleans, small enums, field tags, small message sizes/types).
if ((buffer[0] & 0x80) == 0) [[likely]]
return {buffer[0], 1};
return parse_slow(buffer, len);
}
/// Parse a varint from buffer (safe for empty buffers).
/// Returns result with consumed=0 on failure (empty buffer or truncated varint).
static inline ProtoVarIntResult ESPHOME_ALWAYS_INLINE parse(const uint8_t *buffer, uint32_t len) {
if (len == 0)
return {0, PROTO_VARINT_PARSE_FAILED};
return parse_non_empty(buffer, len);
}
#ifdef USE_API_VARINT64
protected:
// Slow path for multi-byte varints (>= 128), outlined to keep fast path small
static ProtoVarIntResult parse_slow(const uint8_t *buffer, uint32_t len) __attribute__((noinline));
#ifdef USE_API_VARINT64
/// Continue parsing varint bytes 4-9 with 64-bit arithmetic.
/// Separated to keep 64-bit shift code (__ashldi3 on 32-bit platforms) out of the common path.
static optional<ProtoVarInt> parse_wide(const uint8_t *buffer, uint32_t len, uint32_t *consumed, uint32_t result32)
__attribute__((noinline));
public:
#endif
constexpr uint16_t as_uint16() const { return this->value_; }
constexpr uint32_t as_uint32() const { return this->value_; }
constexpr bool as_bool() const { return this->value_; }
constexpr int32_t as_int32() const {
// Not ZigZag encoded
return static_cast<int32_t>(this->value_);
}
constexpr int32_t as_sint32() const {
// with ZigZag encoding
return decode_zigzag32(static_cast<uint32_t>(this->value_));
}
#ifdef USE_API_VARINT64
constexpr uint64_t as_uint64() const { return this->value_; }
constexpr int64_t as_int64() const {
// Not ZigZag encoded
return static_cast<int64_t>(this->value_);
}
constexpr int64_t as_sint64() const {
// with ZigZag encoding
return decode_zigzag64(this->value_);
}
#endif
protected:
#ifdef USE_API_VARINT64
uint64_t value_;
#else
uint32_t value_;
static ProtoVarIntResult parse_wide(const uint8_t *buffer, uint32_t len, uint32_t result32) __attribute__((noinline));
#endif
};
@@ -237,9 +204,8 @@ class Proto32Bit {
class ProtoWriteBuffer {
public:
ProtoWriteBuffer(std::vector<uint8_t> *buffer) : buffer_(buffer), pos_(buffer->data() + buffer->size()) {}
ProtoWriteBuffer(std::vector<uint8_t> *buffer, size_t write_pos)
: buffer_(buffer), pos_(buffer->data() + write_pos) {}
ProtoWriteBuffer(APIBuffer *buffer) : buffer_(buffer), pos_(buffer->data() + buffer->size()) {}
ProtoWriteBuffer(APIBuffer *buffer, size_t write_pos) : buffer_(buffer), pos_(buffer->data() + write_pos) {}
inline void ESPHOME_ALWAYS_INLINE encode_varint_raw(uint32_t value) {
if (value < 128) [[likely]] {
this->debug_check_bounds_(1);
@@ -374,7 +340,7 @@ class ProtoWriteBuffer {
// Non-template core for encode_optional_sub_message.
void encode_optional_sub_message(uint32_t field_id, uint32_t nested_size, const void *value,
void (*encode_fn)(const void *, ProtoWriteBuffer &));
std::vector<uint8_t> *get_buffer() const { return buffer_; }
APIBuffer *get_buffer() const { return buffer_; }
protected:
// Slow path for encode_varint_raw values >= 128, outlined to keep fast path small
@@ -387,7 +353,7 @@ class ProtoWriteBuffer {
void debug_check_bounds_([[maybe_unused]] size_t bytes) {}
#endif
std::vector<uint8_t> *buffer_;
APIBuffer *buffer_;
uint8_t *pos_;
};
@@ -499,7 +465,7 @@ class ProtoDecodableMessage : public ProtoMessage {
protected:
~ProtoDecodableMessage() = default;
virtual bool decode_varint(uint32_t field_id, ProtoVarInt value) { return false; }
virtual bool decode_varint(uint32_t field_id, proto_varint_value_t value) { return false; }
virtual bool decode_length(uint32_t field_id, ProtoLengthDelimited value) { return false; }
virtual bool decode_32bit(uint32_t field_id, Proto32Bit value) { return false; }
// NOTE: decode_64bit removed - wire type 1 not supported
+2
View File
@@ -89,6 +89,7 @@ AT581XSettingsAction = at581x_ns.class_("AT581XSettingsAction", automation.Actio
cv.Required(CONF_ID): cv.use_id(AT581XComponent),
}
),
synchronous=True,
)
async def at581x_reset_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
@@ -160,6 +161,7 @@ RADAR_SETTINGS_SCHEMA = cv.Schema(
"at581x.settings",
AT581XSettingsAction,
RADAR_SETTINGS_SCHEMA,
synchronous=True,
)
async def at581x_settings_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
+4 -1
View File
@@ -23,7 +23,10 @@ SET_MIC_GAIN_ACTION_SCHEMA = cv.maybe_simple_value(
@automation.register_action(
"audio_adc.set_mic_gain", SetMicGainAction, SET_MIC_GAIN_ACTION_SCHEMA
"audio_adc.set_mic_gain",
SetMicGainAction,
SET_MIC_GAIN_ACTION_SCHEMA,
synchronous=True,
)
async def audio_adc_set_mic_gain_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
+10 -3
View File
@@ -31,15 +31,22 @@ SET_VOLUME_ACTION_SCHEMA = cv.maybe_simple_value(
)
@automation.register_action("audio_dac.mute_off", MuteOffAction, MUTE_ACTION_SCHEMA)
@automation.register_action("audio_dac.mute_on", MuteOnAction, MUTE_ACTION_SCHEMA)
@automation.register_action(
"audio_dac.mute_off", MuteOffAction, MUTE_ACTION_SCHEMA, synchronous=True
)
@automation.register_action(
"audio_dac.mute_on", MuteOnAction, MUTE_ACTION_SCHEMA, synchronous=True
)
async def audio_dac_mute_action_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action(
"audio_dac.set_volume", SetVolumeAction, SET_VOLUME_ACTION_SCHEMA
"audio_dac.set_volume",
SetVolumeAction,
SET_VOLUME_ACTION_SCHEMA,
synchronous=True,
)
async def audio_dac_set_volume_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -685,6 +685,7 @@ async def to_code(config):
},
key=CONF_ID,
),
synchronous=True,
)
async def binary_sensor_invalidate_state_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
+1
View File
@@ -143,6 +143,7 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
cv.Required(CONF_ID): cv.use_id(BL0906),
}
),
synchronous=True,
)
async def reset_energy_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
+1 -3
View File
@@ -69,10 +69,8 @@ class BL0940 : public PollingComponent, public uart::UARTDevice {
void set_energy_calibration_number(number::Number *num) { this->energy_calibration_number_ = num; }
#endif
#ifdef USE_BUTTON
// Resets all calibration values to defaults (can be triggered by a button)
// Resets all calibration values to defaults
void reset_calibration();
#endif
// Core component methods
void loop() override;
+18 -4
View File
@@ -172,7 +172,10 @@ BLE_REMOVE_BOND_ACTION_SCHEMA = cv.Schema(
@automation.register_action(
"ble_client.disconnect", BLEDisconnectAction, BLE_CONNECT_ACTION_SCHEMA
"ble_client.disconnect",
BLEDisconnectAction,
BLE_CONNECT_ACTION_SCHEMA,
synchronous=False,
)
async def ble_disconnect_to_code(config, action_id, template_arg, args):
parent = await cg.get_variable(config[CONF_ID])
@@ -180,7 +183,10 @@ async def ble_disconnect_to_code(config, action_id, template_arg, args):
@automation.register_action(
"ble_client.connect", BLEConnectAction, BLE_CONNECT_ACTION_SCHEMA
"ble_client.connect",
BLEConnectAction,
BLE_CONNECT_ACTION_SCHEMA,
synchronous=False,
)
async def ble_connect_to_code(config, action_id, template_arg, args):
parent = await cg.get_variable(config[CONF_ID])
@@ -188,7 +194,10 @@ async def ble_connect_to_code(config, action_id, template_arg, args):
@automation.register_action(
"ble_client.ble_write", BLEWriteAction, BLE_WRITE_ACTION_SCHEMA
"ble_client.ble_write",
BLEWriteAction,
BLE_WRITE_ACTION_SCHEMA,
synchronous=False,
)
async def ble_write_to_code(config, action_id, template_arg, args):
parent = await cg.get_variable(config[CONF_ID])
@@ -247,6 +256,7 @@ async def ble_write_to_code(config, action_id, template_arg, args):
"ble_client.numeric_comparison_reply",
BLENumericComparisonReplyAction,
BLE_NUMERIC_COMPARISON_REPLY_ACTION_SCHEMA,
synchronous=True,
)
async def numeric_comparison_reply_to_code(config, action_id, template_arg, args):
parent = await cg.get_variable(config[CONF_ID])
@@ -263,7 +273,10 @@ async def numeric_comparison_reply_to_code(config, action_id, template_arg, args
@automation.register_action(
"ble_client.passkey_reply", BLEPasskeyReplyAction, BLE_PASSKEY_REPLY_ACTION_SCHEMA
"ble_client.passkey_reply",
BLEPasskeyReplyAction,
BLE_PASSKEY_REPLY_ACTION_SCHEMA,
synchronous=True,
)
async def passkey_reply_to_code(config, action_id, template_arg, args):
parent = await cg.get_variable(config[CONF_ID])
@@ -283,6 +296,7 @@ async def passkey_reply_to_code(config, action_id, template_arg, args):
"ble_client.remove_bond",
BLERemoveBondAction,
BLE_REMOVE_BOND_ACTION_SCHEMA,
synchronous=True,
)
async def remove_bond_to_code(config, action_id, template_arg, args):
parent = await cg.get_variable(config[CONF_ID])
+3
View File
@@ -33,6 +33,7 @@ CONFIG_SCHEMA = (
cv.GenerateID(): cv.use_id(BM8563),
}
),
synchronous=True,
)
async def bm8563_write_time_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
@@ -49,6 +50,7 @@ async def bm8563_write_time_to_code(config, action_id, template_arg, args):
cv.Required(CONF_DURATION): cv.templatable(cv.positive_time_period_seconds),
}
),
synchronous=True,
)
async def bm8563_start_timer_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
@@ -66,6 +68,7 @@ async def bm8563_start_timer_to_code(config, action_id, template_arg, args):
cv.GenerateID(): cv.use_id(BM8563),
}
),
synchronous=True,
)
async def bm8563_read_time_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
@@ -7,7 +7,7 @@
#include <esphome/components/sensor/sensor.h>
#include <esphome/core/component.h>
#define BME280_ERROR_WRONG_CHIP_ID "Wrong chip ID"
#define BME280_ERROR_WRONG_CHIP_ID "Wrong chip ID or no response"
namespace esphome {
namespace bme280_base {
@@ -2,7 +2,7 @@
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#define BMP280_ERROR_WRONG_CHIP_ID "Wrong chip ID"
#define BMP280_ERROR_WRONG_CHIP_ID "Wrong chip ID or no response"
namespace esphome {
namespace bmp280_base {
+1
View File
@@ -155,6 +155,7 @@ async def register_canbus(var, config):
validate_id,
key=CONF_DATA,
),
synchronous=True,
)
async def canbus_action_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
+19 -8
View File
@@ -287,10 +287,18 @@ CC1101_ACTION_SCHEMA = cv.Schema(
)
@automation.register_action("cc1101.begin_tx", BeginTxAction, CC1101_ACTION_SCHEMA)
@automation.register_action("cc1101.begin_rx", BeginRxAction, CC1101_ACTION_SCHEMA)
@automation.register_action("cc1101.reset", ResetAction, CC1101_ACTION_SCHEMA)
@automation.register_action("cc1101.set_idle", SetIdleAction, CC1101_ACTION_SCHEMA)
@automation.register_action(
"cc1101.begin_tx", BeginTxAction, CC1101_ACTION_SCHEMA, synchronous=True
)
@automation.register_action(
"cc1101.begin_rx", BeginRxAction, CC1101_ACTION_SCHEMA, synchronous=True
)
@automation.register_action(
"cc1101.reset", ResetAction, CC1101_ACTION_SCHEMA, synchronous=True
)
@automation.register_action(
"cc1101.set_idle", SetIdleAction, CC1101_ACTION_SCHEMA, synchronous=True
)
async def cc1101_action_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
@@ -317,7 +325,10 @@ SEND_PACKET_ACTION_SCHEMA = cv.maybe_simple_value(
@automation.register_action(
"cc1101.send_packet", SendPacketAction, SEND_PACKET_ACTION_SCHEMA
"cc1101.send_packet",
SendPacketAction,
SEND_PACKET_ACTION_SCHEMA,
synchronous=True,
)
async def send_packet_action_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
@@ -419,9 +430,9 @@ def _register_setter_actions():
cg.add(getattr(var, _setter)(_map[data] if _map else data))
return var
automation.register_action(f"cc1101.{setter_name}", action_cls, schema)(
_setter_action_to_code
)
automation.register_action(
f"cc1101.{setter_name}", action_cls, schema, synchronous=True
)(_setter_action_to_code)
_register_setter_actions()
+4 -1
View File
@@ -476,7 +476,10 @@ CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema(
@automation.register_action(
"climate.control", ControlAction, CLIMATE_CONTROL_ACTION_SCHEMA
"climate.control",
ControlAction,
CLIMATE_CONTROL_ACTION_SCHEMA,
synchronous=True,
)
async def climate_control_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
+1
View File
@@ -65,6 +65,7 @@ CALIBRATION_ACTION_SCHEMA = maybe_simple_id(
"cm1106.calibrate_zero",
CM1106CalibrateZeroAction,
CALIBRATION_ACTION_SCHEMA,
synchronous=True,
)
async def cm1106_calibration_to_code(config, action_id, template_arg, args) -> None:
"""Service code generation entry point."""
+1
View File
@@ -132,6 +132,7 @@ async def to_code(config):
cv.Required(CONF_ID): cv.use_id(CS5460AComponent),
}
),
synchronous=True,
)
async def restart_action_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
+2 -3
View File
@@ -91,11 +91,10 @@ void DaikinArcClimate::transmit_state() {
remote_state[5] = this->operation_mode_() | 0x08;
remote_state[6] = this->temperature_();
remote_state[7] = this->humidity_();
static uint8_t last_humidity = 0x66;
if (remote_state[7] != last_humidity && this->mode != climate::CLIMATE_MODE_OFF) {
if (remote_state[7] != this->last_humidity_ && this->mode != climate::CLIMATE_MODE_OFF) {
ESP_LOGD(TAG, "Set Humditiy: %d, %d\n", (int) this->target_humidity, (int) remote_state[7]);
remote_header[9] |= 0x10;
last_humidity = remote_state[7];
this->last_humidity_ = remote_state[7];
}
uint16_t fan_speed = this->fan_speed_();
remote_state[8] = fan_speed >> 8;
@@ -70,6 +70,7 @@ class DaikinArcClimate : public climate_ir::ClimateIR {
// Handle received IR Buffer
bool on_receive(remote_base::RemoteReceiveData data) override;
bool parse_state_frame_(const uint8_t frame[]);
uint8_t last_humidity_{0x66};
};
} // namespace daikin_arc
+3
View File
@@ -187,6 +187,7 @@ async def to_code(config):
),
}
),
synchronous=True,
)
async def datetime_date_set_to_code(config, action_id, template_arg, args):
action_var = cg.new_Pvariable(action_id, template_arg)
@@ -218,6 +219,7 @@ async def datetime_date_set_to_code(config, action_id, template_arg, args):
),
}
),
synchronous=True,
)
async def datetime_time_set_to_code(config, action_id, template_arg, args):
action_var = cg.new_Pvariable(action_id, template_arg)
@@ -249,6 +251,7 @@ async def datetime_time_set_to_code(config, action_id, template_arg, args):
),
},
),
synchronous=True,
)
async def datetime_datetime_set_to_code(config, action_id, template_arg, args):
action_var = cg.new_Pvariable(action_id, template_arg)
+6 -1
View File
@@ -405,7 +405,10 @@ DEEP_SLEEP_ENTER_SCHEMA = cv.All(
@automation.register_action(
"deep_sleep.enter", EnterDeepSleepAction, DEEP_SLEEP_ENTER_SCHEMA
"deep_sleep.enter",
EnterDeepSleepAction,
DEEP_SLEEP_ENTER_SCHEMA,
synchronous=True,
)
async def deep_sleep_enter_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -428,11 +431,13 @@ async def deep_sleep_enter_to_code(config, action_id, template_arg, args):
"deep_sleep.prevent",
PreventDeepSleepAction,
automation.maybe_simple_id(DEEP_SLEEP_ACTION_SCHEMA),
synchronous=True,
)
@automation.register_action(
"deep_sleep.allow",
AllowDeepSleepAction,
automation.maybe_simple_id(DEEP_SLEEP_ACTION_SCHEMA),
synchronous=True,
)
async def deep_sleep_action_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
+1
View File
@@ -0,0 +1 @@
CODEOWNERS = ["@CFlix"]
@@ -0,0 +1,82 @@
#include "dew_point.h"
namespace esphome::dew_point {
static const char *const TAG = "dew_point.sensor";
void DewPointComponent::setup() {
// Register callbacks for sensor updates
if (this->temperature_sensor_ != nullptr) {
this->temperature_sensor_->add_on_state_callback([this](float state) {
this->temperature_value_ = state;
this->enable_loop();
});
// Get initial value
if (this->temperature_sensor_->has_state()) {
this->temperature_value_ = this->temperature_sensor_->get_state();
}
}
if (this->humidity_sensor_ != nullptr) {
this->humidity_sensor_->add_on_state_callback([this](float state) {
this->humidity_value_ = state;
this->enable_loop();
});
// Get initial value
if (this->humidity_sensor_->has_state()) {
this->humidity_value_ = this->humidity_sensor_->get_state();
}
}
}
void DewPointComponent::dump_config() {
LOG_SENSOR("", "Dew Point", this);
ESP_LOGCONFIG(TAG,
"Sources\n"
" Temperature: '%s'\n"
" Humidity: '%s'",
this->temperature_sensor_->get_name().c_str(), this->humidity_sensor_->get_name().c_str());
}
float DewPointComponent::get_setup_priority() const { return setup_priority::DATA; }
void DewPointComponent::loop() {
// Only run once
this->disable_loop();
// Check if we have valid values for both sensors
if (std::isnan(this->temperature_value_) || std::isnan(this->humidity_value_)) {
ESP_LOGW(TAG, "Temperature or humidity value is NaN, skipping calculation");
this->publish_state(NAN);
return;
}
// Check for valid humidity range
if (this->humidity_value_ <= 0.0f || this->humidity_value_ > 100.0f) {
ESP_LOGW(TAG, "Humidity value out of range (0-100): %.2f", this->humidity_value_);
this->publish_state(NAN);
return;
}
// Magnus formula constants
const float a{17.625f};
const float b{243.04f};
// Calculate dew point using Magnus formula
// Td = (b * alpha) / (a - alpha)
// where alpha = ln(RH/100) + (a * T) / (b + T)
const float alpha{std::log(this->humidity_value_ / 100.0f) +
(a * this->temperature_value_) / (b + this->temperature_value_)};
const float dew_point{(b * alpha) / (a - alpha)};
// Publish the calculated dew point
this->publish_state(dew_point);
ESP_LOGD(TAG, "'%s' >> %.1f°C (T: %.1f°C, RH: %.1f%%)", this->get_name().c_str(), dew_point, this->temperature_value_,
this->humidity_value_);
}
} // namespace esphome::dew_point
+26
View File
@@ -0,0 +1,26 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
namespace esphome::dew_point {
class DewPointComponent : public Component, public sensor::Sensor {
public:
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; }
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
void setup() override;
void dump_config() override;
void loop() override;
float get_setup_priority() const override;
protected:
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *humidity_sensor_{nullptr};
float temperature_value_{NAN};
float humidity_value_{NAN};
};
} // namespace esphome::dew_point
+46
View File
@@ -0,0 +1,46 @@
import esphome.codegen as cg
from esphome.components import sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_HUMIDITY,
CONF_TEMPERATURE,
DEVICE_CLASS_TEMPERATURE,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
)
DEPENDENCIES = ["sensor"]
dew_point_ns = cg.esphome_ns.namespace("dew_point")
DewPointComponent = dew_point_ns.class_(
"DewPointComponent", cg.Component, sensor.Sensor
)
CONFIG_SCHEMA = (
sensor.sensor_schema(
DewPointComponent,
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
icon="mdi:weather-rainy",
)
.extend(
{
cv.Required(CONF_TEMPERATURE): cv.use_id(sensor.Sensor),
cv.Required(CONF_HUMIDITY): cv.use_id(sensor.Sensor),
}
)
.extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config):
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
temperature_sensor = await cg.get_variable(config[CONF_TEMPERATURE])
cg.add(var.set_temperature_sensor(temperature_sensor))
humidity_sensor = await cg.get_variable(config[CONF_HUMIDITY])
cg.add(var.set_humidity_sensor(humidity_sensor))
+16
View File
@@ -91,6 +91,7 @@ async def to_code(config):
cv.GenerateID(): cv.use_id(DFPlayer),
}
),
synchronous=True,
)
async def dfplayer_next_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
@@ -106,6 +107,7 @@ async def dfplayer_next_to_code(config, action_id, template_arg, args):
cv.GenerateID(): cv.use_id(DFPlayer),
}
),
synchronous=True,
)
async def dfplayer_previous_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
@@ -123,6 +125,7 @@ async def dfplayer_previous_to_code(config, action_id, template_arg, args):
},
key=CONF_FILE,
),
synchronous=True,
)
async def dfplayer_play_mp3_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
@@ -143,6 +146,7 @@ async def dfplayer_play_mp3_to_code(config, action_id, template_arg, args):
},
key=CONF_FILE,
),
synchronous=True,
)
async def dfplayer_play_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
@@ -166,6 +170,7 @@ async def dfplayer_play_to_code(config, action_id, template_arg, args):
cv.Optional(CONF_LOOP): cv.templatable(cv.boolean),
}
),
synchronous=True,
)
async def dfplayer_play_folder_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
@@ -191,6 +196,7 @@ async def dfplayer_play_folder_to_code(config, action_id, template_arg, args):
},
key=CONF_DEVICE,
),
synchronous=True,
)
async def dfplayer_set_device_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
@@ -210,6 +216,7 @@ async def dfplayer_set_device_to_code(config, action_id, template_arg, args):
},
key=CONF_VOLUME,
),
synchronous=True,
)
async def dfplayer_set_volume_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
@@ -227,6 +234,7 @@ async def dfplayer_set_volume_to_code(config, action_id, template_arg, args):
cv.GenerateID(): cv.use_id(DFPlayer),
}
),
synchronous=True,
)
async def dfplayer_volume_up_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
@@ -242,6 +250,7 @@ async def dfplayer_volume_up_to_code(config, action_id, template_arg, args):
cv.GenerateID(): cv.use_id(DFPlayer),
}
),
synchronous=True,
)
async def dfplayer_volume_down_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
@@ -259,6 +268,7 @@ async def dfplayer_volume_down_to_code(config, action_id, template_arg, args):
},
key=CONF_EQ_PRESET,
),
synchronous=True,
)
async def dfplayer_set_eq_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
@@ -276,6 +286,7 @@ async def dfplayer_set_eq_to_code(config, action_id, template_arg, args):
cv.GenerateID(): cv.use_id(DFPlayer),
}
),
synchronous=True,
)
async def dfplayer_sleep_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
@@ -291,6 +302,7 @@ async def dfplayer_sleep_to_code(config, action_id, template_arg, args):
cv.GenerateID(): cv.use_id(DFPlayer),
}
),
synchronous=True,
)
async def dfplayer_reset_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
@@ -306,6 +318,7 @@ async def dfplayer_reset_to_code(config, action_id, template_arg, args):
cv.GenerateID(): cv.use_id(DFPlayer),
}
),
synchronous=True,
)
async def dfplayer_start_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
@@ -321,6 +334,7 @@ async def dfplayer_start_to_code(config, action_id, template_arg, args):
cv.GenerateID(): cv.use_id(DFPlayer),
}
),
synchronous=True,
)
async def dfplayer_pause_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
@@ -336,6 +350,7 @@ async def dfplayer_pause_to_code(config, action_id, template_arg, args):
cv.GenerateID(): cv.use_id(DFPlayer),
}
),
synchronous=True,
)
async def dfplayer_stop_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
@@ -351,6 +366,7 @@ async def dfplayer_stop_to_code(config, action_id, template_arg, args):
cv.GenerateID(): cv.use_id(DFPlayer),
}
),
synchronous=True,
)
async def dfplayer_random_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
@@ -52,6 +52,7 @@ async def to_code(config):
cv.GenerateID(): cv.use_id(DfrobotSen0395Component),
}
),
synchronous=True,
)
async def dfrobot_sen0395_reset_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
@@ -151,6 +152,7 @@ MMWAVE_SETTINGS_SCHEMA = cv.Schema(
"dfrobot_sen0395.settings",
DfrobotSen0395SettingsAction,
MMWAVE_SETTINGS_SCHEMA,
synchronous=True,
)
async def dfrobot_sen0395_settings_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
+3
View File
@@ -159,6 +159,7 @@ async def register_display(var, config):
cv.Required(CONF_ID): cv.templatable(cv.use_id(DisplayPage)),
}
),
synchronous=True,
)
async def display_page_show_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
@@ -179,6 +180,7 @@ async def display_page_show_to_code(config, action_id, template_arg, args):
cv.GenerateID(CONF_ID): cv.templatable(cv.use_id(Display)),
}
),
synchronous=True,
)
async def display_page_show_next_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -193,6 +195,7 @@ async def display_page_show_next_to_code(config, action_id, template_arg, args):
cv.GenerateID(CONF_ID): cv.templatable(cv.use_id(Display)),
}
),
synchronous=True,
)
async def display_page_show_previous_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -294,50 +294,67 @@ MENU_ACTION_SCHEMA = maybe_simple_id(
)
@automation.register_action("display_menu.up", UpAction, MENU_ACTION_SCHEMA)
@automation.register_action(
"display_menu.up", UpAction, MENU_ACTION_SCHEMA, synchronous=True
)
async def menu_up_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action("display_menu.down", DownAction, MENU_ACTION_SCHEMA)
@automation.register_action(
"display_menu.down", DownAction, MENU_ACTION_SCHEMA, synchronous=True
)
async def menu_down_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action("display_menu.left", LeftAction, MENU_ACTION_SCHEMA)
@automation.register_action(
"display_menu.left", LeftAction, MENU_ACTION_SCHEMA, synchronous=True
)
async def menu_left_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action("display_menu.right", RightAction, MENU_ACTION_SCHEMA)
@automation.register_action(
"display_menu.right", RightAction, MENU_ACTION_SCHEMA, synchronous=True
)
async def menu_right_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action("display_menu.enter", EnterAction, MENU_ACTION_SCHEMA)
@automation.register_action(
"display_menu.enter", EnterAction, MENU_ACTION_SCHEMA, synchronous=True
)
async def menu_enter_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action("display_menu.show", ShowAction, MENU_ACTION_SCHEMA)
@automation.register_action(
"display_menu.show", ShowAction, MENU_ACTION_SCHEMA, synchronous=True
)
async def menu_show_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action("display_menu.hide", HideAction, MENU_ACTION_SCHEMA)
@automation.register_action(
"display_menu.hide", HideAction, MENU_ACTION_SCHEMA, synchronous=True
)
async def menu_hide_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action(
"display_menu.show_main", ShowMainAction, MENU_ACTION_SCHEMA
"display_menu.show_main",
ShowMainAction,
MENU_ACTION_SCHEMA,
synchronous=True,
)
async def menu_show_main_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
+2
View File
@@ -27,6 +27,7 @@ CONFIG_SCHEMA = time.TIME_SCHEMA.extend(
cv.GenerateID(): cv.use_id(DS1307Component),
}
),
synchronous=True,
)
async def ds1307_write_time_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
@@ -42,6 +43,7 @@ async def ds1307_write_time_to_code(config, action_id, template_arg, args):
cv.GenerateID(): cv.use_id(DS1307Component),
}
),
synchronous=True,
)
async def ds1307_read_time_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
+9 -3
View File
@@ -90,21 +90,27 @@ DUTY_TIME_ID_SCHEMA = maybe_simple_id(
)
@register_action("sensor.duty_time.start", StartAction, DUTY_TIME_ID_SCHEMA)
@register_action(
"sensor.duty_time.start", StartAction, DUTY_TIME_ID_SCHEMA, synchronous=True
)
async def sensor_runtime_start_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var
@register_action("sensor.duty_time.stop", StopAction, DUTY_TIME_ID_SCHEMA)
@register_action(
"sensor.duty_time.stop", StopAction, DUTY_TIME_ID_SCHEMA, synchronous=True
)
async def sensor_runtime_stop_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var
@register_action("sensor.duty_time.reset", ResetAction, DUTY_TIME_ID_SCHEMA)
@register_action(
"sensor.duty_time.reset", ResetAction, DUTY_TIME_ID_SCHEMA, synchronous=True
)
async def sensor_runtime_reset_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
+5
View File
@@ -1442,6 +1442,11 @@ async def to_code(config):
cg.add_build_flag("-DUSE_ESP32")
cg.add_define("USE_NATIVE_64BIT_TIME")
cg.add_build_flag("-Wl,-z,noexecstack")
# Arduino already wraps esp_panic_handler for its own backtrace handler,
# so only add our wrap when using ESP-IDF framework to avoid linker conflicts.
if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
cg.add_build_flag("-Wl,--wrap=esp_panic_handler")
cg.add_define("USE_ESP32_CRASH_HANDLER")
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
variant = config[CONF_VARIANT]
cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{variant}")
+6
View File
@@ -1,6 +1,7 @@
#ifdef USE_ESP32
#include "esphome/core/defines.h"
#include "crash_handler.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "preferences.h"
@@ -36,6 +37,11 @@ void arch_restart() {
}
void arch_init() {
#ifdef USE_ESP32_CRASH_HANDLER
// Read crash data from previous boot before anything else
esp32::crash_handler_read_and_clear();
#endif
// Enable the task watchdog only on the loop task (from which we're currently running)
esp_task_wdt_add(nullptr);
+355
View File
@@ -0,0 +1,355 @@
#ifdef USE_ESP32
#include "esphome/core/defines.h"
#ifdef USE_ESP32_CRASH_HANDLER
#include "crash_handler.h"
#include "esphome/core/log.h"
#include <cinttypes>
#include <cstring>
#include <esp_attr.h>
#include <esp_private/panic_internal.h>
#include <soc/soc.h>
#if CONFIG_IDF_TARGET_ARCH_XTENSA
#include <esp_cpu_utils.h>
#include <esp_debug_helpers.h>
#include <xtensa_context.h>
#elif CONFIG_IDF_TARGET_ARCH_RISCV
#include <riscv/rvruntime-frames.h>
#endif
static constexpr uint32_t CRASH_MAGIC = 0xDEADBEEF;
static constexpr size_t MAX_BACKTRACE = 16;
// Check if an address looks like code (flash-mapped or IRAM).
// Must be safe to call from panic context (no flash access needed).
static inline bool IRAM_ATTR is_code_addr(uint32_t addr) {
return (addr >= SOC_IROM_LOW && addr < SOC_IROM_HIGH) || (addr >= SOC_IRAM_LOW && addr < SOC_IRAM_HIGH);
}
#if CONFIG_IDF_TARGET_ARCH_RISCV
// Check if a code address is a real return address by verifying the preceding
// instruction is a JAL or JALR with rd=ra (x1). Called at log time (not during
// panic) so flash cache is available and both IRAM and IROM are safely readable.
static inline bool is_return_addr(uint32_t addr) {
if (!is_code_addr(addr) || addr < 4)
return false;
// A return address on the stack points to the instruction after a call.
// Check for 4-byte JAL/JALR call instruction before this address.
// Use memcpy for alignment safety — RISC-V C extension means code addresses
// are only 2-byte aligned, so addr-4 may not be 4-byte aligned.
uint32_t inst;
memcpy(&inst, (const void *) (addr - 4), sizeof(inst));
// RISC-V instruction encoding: bits [6:0] = opcode, bits [11:7] = rd
uint32_t opcode = inst & 0x7f; // Extract 7-bit opcode
uint32_t rd = inst & 0xf80; // Extract rd field (bits 11:7)
// Match JAL (0x6f) or JALR (0x67) with rd=ra (x1, encoded as 0x80 = 1<<7)
if ((opcode == 0x6f || opcode == 0x67) && rd == 0x80)
return true;
// Check for 2-byte compressed c.jalr before this address (C extension).
// c.jalr saves to ra implicitly: funct4=1001, rs1!=0, rs2=0, op=10
if (addr >= 2) {
uint16_t c_inst = *(uint16_t *) (addr - 2);
if ((c_inst & 0xf07f) == 0x9002 && (c_inst & 0x0f80) != 0)
return true;
}
return false;
}
#endif
// Raw crash data written by the panic handler wrapper.
// Lives in .noinit so it survives software reset but contains garbage after power cycle.
// Validated by magic marker. Static linkage since it's only used within this file.
// Version field is first so future firmware can always identify the struct layout.
// Magic is second to validate the data. Remaining fields can change between versions.
// Version is uint32_t because it would be padded to 4 bytes anyway before the next
// uint32_t field, so we use the full width rather than wasting 3 bytes of padding.
static constexpr uint32_t CRASH_DATA_VERSION = 1;
struct RawCrashData {
uint32_t version;
uint32_t magic;
uint32_t pc;
uint8_t backtrace_count;
uint8_t reg_frame_count; // Number of entries from registers (not stack-scanned)
uint8_t exception; // panic_exception_t enum (FAULT/ABORT/IWDT/TWDT/DEBUG)
uint8_t pseudo_excause; // Whether cause is a pseudo exception (Xtensa SoC-level panic)
uint32_t backtrace[MAX_BACKTRACE];
uint32_t cause; // Architecture-specific: exccause (Xtensa) or mcause (RISC-V)
};
static RawCrashData __attribute__((section(".noinit")))
s_raw_crash_data; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
// Whether crash data was found and validated this boot.
static bool s_crash_data_valid = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
namespace esphome::esp32 {
static const char *const TAG = "esp32.crash";
void crash_handler_read_and_clear() {
if (s_raw_crash_data.magic == CRASH_MAGIC && s_raw_crash_data.version == CRASH_DATA_VERSION) {
s_crash_data_valid = true;
// Clamp counts to prevent out-of-bounds reads from corrupt .noinit data
if (s_raw_crash_data.backtrace_count > MAX_BACKTRACE)
s_raw_crash_data.backtrace_count = MAX_BACKTRACE;
if (s_raw_crash_data.reg_frame_count > s_raw_crash_data.backtrace_count)
s_raw_crash_data.reg_frame_count = s_raw_crash_data.backtrace_count;
if (s_raw_crash_data.exception > 4) // panic_exception_t max value
s_raw_crash_data.exception = 4; // Default to PANIC_EXCEPTION_FAULT
if (s_raw_crash_data.pseudo_excause > 1)
s_raw_crash_data.pseudo_excause = 0;
}
// Clear magic regardless so we don't re-report on next normal reboot
s_raw_crash_data.magic = 0;
}
bool crash_handler_has_data() { return s_crash_data_valid; }
// Look up the exception cause as a human-readable string.
// Tables mirror ESP-IDF's panic_arch_fill_info() which uses local static arrays
// not exposed via any public API.
static const char *get_exception_reason() {
#if CONFIG_IDF_TARGET_ARCH_XTENSA
if (s_raw_crash_data.pseudo_excause) {
// SoC-level panic: watchdog, cache error, etc.
// Keep in sync with ESP-IDF's PANIC_RSN_* defines
static const char *const PSEUDO_REASON[] = {
"Unknown reason", // 0
"Unhandled debug exception", // 1
"Double exception", // 2
"Unhandled kernel exception", // 3
"Coprocessor exception", // 4
"Interrupt wdt timeout on CPU0", // 5
"Interrupt wdt timeout on CPU1", // 6
"Cache error", // 7
};
uint32_t cause = s_raw_crash_data.cause;
if (cause < sizeof(PSEUDO_REASON) / sizeof(PSEUDO_REASON[0]))
return PSEUDO_REASON[cause];
return PSEUDO_REASON[0];
}
// Real Xtensa exception
static const char *const REASON[] = {
"IllegalInstruction",
"Syscall",
"InstructionFetchError",
"LoadStoreError",
"Level1Interrupt",
"Alloca",
"IntegerDivideByZero",
"PCValue",
"Privileged",
"LoadStoreAlignment",
nullptr,
nullptr,
"InstrPDAddrError",
"LoadStorePIFDataError",
"InstrPIFAddrError",
"LoadStorePIFAddrError",
"InstTLBMiss",
"InstTLBMultiHit",
"InstFetchPrivilege",
nullptr,
"InstrFetchProhibited",
nullptr,
nullptr,
nullptr,
"LoadStoreTLBMiss",
"LoadStoreTLBMultihit",
"LoadStorePrivilege",
nullptr,
"LoadProhibited",
"StoreProhibited",
};
uint32_t cause = s_raw_crash_data.cause;
if (cause < sizeof(REASON) / sizeof(REASON[0]) && REASON[cause] != nullptr)
return REASON[cause];
#elif CONFIG_IDF_TARGET_ARCH_RISCV
// For SoC-level panics (watchdog, cache error), mcause holds IDF-internal
// interrupt numbers, not standard RISC-V cause codes. The exception type
// field already identifies these, so just return null to use the type name.
if (s_raw_crash_data.pseudo_excause)
return nullptr;
static const char *const REASON[] = {
"Instruction address misaligned",
"Instruction access fault",
"Illegal instruction",
"Breakpoint",
"Load address misaligned",
"Load access fault",
"Store address misaligned",
"Store access fault",
"Environment call from U-mode",
"Environment call from S-mode",
nullptr,
"Environment call from M-mode",
"Instruction page fault",
"Load page fault",
nullptr,
"Store page fault",
};
uint32_t cause = s_raw_crash_data.cause;
if (cause < sizeof(REASON) / sizeof(REASON[0]) && REASON[cause] != nullptr)
return REASON[cause];
#endif
return "Unknown";
}
// Exception type names matching panic_exception_t enum
static const char *get_exception_type() {
static const char *const TYPES[] = {
"Debug exception", // PANIC_EXCEPTION_DEBUG
"Interrupt wdt", // PANIC_EXCEPTION_IWDT
"Task wdt", // PANIC_EXCEPTION_TWDT
"Abort", // PANIC_EXCEPTION_ABORT
"Fault", // PANIC_EXCEPTION_FAULT
};
uint8_t exc = s_raw_crash_data.exception;
if (exc < sizeof(TYPES) / sizeof(TYPES[0]))
return TYPES[exc];
return "Unknown";
}
// Intentionally uses separate ESP_LOGE calls per line instead of combining into
// one multi-line log message. This ensures each address appears as its own line
// on the serial console, making it possible to see partial output if the device
// crashes again during boot, and allowing the CLI's process_stacktrace to match
// and decode each address individually.
void crash_handler_log() {
if (!s_crash_data_valid)
return;
ESP_LOGE(TAG, "*** CRASH DETECTED ON PREVIOUS BOOT ***");
const char *reason = get_exception_reason();
if (reason != nullptr) {
ESP_LOGE(TAG, " Reason: %s - %s", get_exception_type(), reason);
} else {
ESP_LOGE(TAG, " Reason: %s", get_exception_type());
}
ESP_LOGE(TAG, " PC: 0x%08" PRIX32 " (fault location)", s_raw_crash_data.pc);
uint8_t bt_num = 0;
for (uint8_t i = 0; i < s_raw_crash_data.backtrace_count; i++) {
uint32_t addr = s_raw_crash_data.backtrace[i];
#if CONFIG_IDF_TARGET_ARCH_RISCV
// Register-sourced entries (MEPC/RA) are trusted; only filter stack-scanned ones.
if (i >= s_raw_crash_data.reg_frame_count && !is_return_addr(addr))
continue;
#endif
#if CONFIG_IDF_TARGET_ARCH_RISCV
const char *source = (i < s_raw_crash_data.reg_frame_count) ? "backtrace" : "stack scan";
#else
const char *source = "backtrace";
#endif
ESP_LOGE(TAG, " BT%d: 0x%08" PRIX32 " (%s)", bt_num++, addr, source);
}
// Build addr2line hint with all captured addresses for easy copy-paste
char hint[256];
int pos = snprintf(hint, sizeof(hint), "Use: addr2line -pfiaC -e firmware.elf 0x%08" PRIX32, s_raw_crash_data.pc);
for (uint8_t i = 0; i < s_raw_crash_data.backtrace_count && pos < (int) sizeof(hint) - 12; i++) {
uint32_t addr = s_raw_crash_data.backtrace[i];
#if CONFIG_IDF_TARGET_ARCH_RISCV
if (i >= s_raw_crash_data.reg_frame_count && !is_return_addr(addr))
continue;
#endif
pos += snprintf(hint + pos, sizeof(hint) - pos, " 0x%08" PRIX32, addr);
}
ESP_LOGE(TAG, "%s", hint);
}
} // namespace esphome::esp32
// --- Panic handler wrapper ---
// Intercepts esp_panic_handler() via --wrap linker flag to capture crash data
// into NOINIT memory before the normal panic handler runs.
//
extern "C" {
// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
// Names are mandated by the --wrap linker mechanism
extern void __real_esp_panic_handler(panic_info_t *info);
void IRAM_ATTR __wrap_esp_panic_handler(panic_info_t *info) {
// Save the faulting PC and exception info
s_raw_crash_data.pc = (uint32_t) info->addr;
s_raw_crash_data.backtrace_count = 0;
s_raw_crash_data.reg_frame_count = 0;
s_raw_crash_data.exception = (uint8_t) info->exception;
s_raw_crash_data.pseudo_excause = info->pseudo_excause ? 1 : 0;
#if CONFIG_IDF_TARGET_ARCH_XTENSA
// Xtensa: walk the backtrace using the public API
if (info->frame != nullptr) {
auto *xt_frame = (XtExcFrame *) info->frame;
s_raw_crash_data.cause = xt_frame->exccause;
esp_backtrace_frame_t bt_frame = {
.pc = (uint32_t) xt_frame->pc,
.sp = (uint32_t) xt_frame->a1,
.next_pc = (uint32_t) xt_frame->a0,
.exc_frame = xt_frame,
};
uint8_t count = 0;
// First frame PC
uint32_t first_pc = esp_cpu_process_stack_pc(bt_frame.pc);
if (is_code_addr(first_pc)) {
s_raw_crash_data.backtrace[count++] = first_pc;
}
// Walk remaining frames
while (count < MAX_BACKTRACE && bt_frame.next_pc != 0) {
if (!esp_backtrace_get_next_frame(&bt_frame)) {
break;
}
uint32_t pc = esp_cpu_process_stack_pc(bt_frame.pc);
if (is_code_addr(pc)) {
s_raw_crash_data.backtrace[count++] = pc;
}
}
s_raw_crash_data.backtrace_count = count;
}
#elif CONFIG_IDF_TARGET_ARCH_RISCV
// RISC-V: capture MEPC + RA, then scan stack for code addresses
if (info->frame != nullptr) {
auto *rv_frame = (RvExcFrame *) info->frame;
s_raw_crash_data.cause = rv_frame->mcause;
uint8_t count = 0;
// Save MEPC (fault PC) and RA (return address)
if (is_code_addr(rv_frame->mepc)) {
s_raw_crash_data.backtrace[count++] = rv_frame->mepc;
}
if (is_code_addr(rv_frame->ra) && rv_frame->ra != rv_frame->mepc) {
s_raw_crash_data.backtrace[count++] = rv_frame->ra;
}
// Track how many entries came from registers (MEPC/RA) so we can
// skip return-address validation for them at log time.
s_raw_crash_data.reg_frame_count = count;
// Scan stack for code addresses — captures broadly during panic,
// filtered by is_return_addr() at log time when flash is accessible.
auto *scan_start = (uint32_t *) rv_frame->sp;
for (uint32_t i = 0; i < 64 && count < MAX_BACKTRACE; i++) {
uint32_t val = scan_start[i];
if (is_code_addr(val) && val != rv_frame->mepc && val != rv_frame->ra) {
s_raw_crash_data.backtrace[count++] = val;
}
}
s_raw_crash_data.backtrace_count = count;
}
#endif
// Write version and magic last — ensures all data is written before we mark it valid
s_raw_crash_data.version = CRASH_DATA_VERSION;
s_raw_crash_data.magic = CRASH_MAGIC;
// Call the real panic handler (prints to UART, does core dump, reboots, etc.)
__real_esp_panic_handler(info);
}
// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
} // extern "C"
#endif // USE_ESP32_CRASH_HANDLER
#endif // USE_ESP32
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#ifdef USE_ESP32_CRASH_HANDLER
namespace esphome::esp32 {
/// Read crash data from NOINIT memory and clear the magic marker.
void crash_handler_read_and_clear();
/// Log crash data if a crash was detected on previous boot.
void crash_handler_log();
/// Returns true if crash data was found this boot.
bool crash_handler_has_data();
} // namespace esphome::esp32
#endif // USE_ESP32_CRASH_HANDLER
+2 -2
View File
@@ -88,8 +88,8 @@ def _translate_pin(value):
@dataclass
class ESP32ValidationFunctions:
pin_validation: Callable[[Any], Any]
usage_validation: Callable[[Any], Any]
pin_validation: Callable[[int], int]
usage_validation: Callable[[dict[str, Any]], dict[str, Any]]
_esp32_validations = {
+3 -2
View File
@@ -1,4 +1,5 @@
import logging
from typing import Any
import esphome.config_validation as cv
from esphome.const import (
@@ -22,7 +23,7 @@ _ESP32_STRAPPING_PINS = {0, 2, 5, 12, 15}
_LOGGER = logging.getLogger(__name__)
def esp32_validate_gpio_pin(value):
def esp32_validate_gpio_pin(value: int) -> int:
if value < 0 or value > 39:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-39)")
if value in _ESP_SDIO_PINS:
@@ -41,7 +42,7 @@ def esp32_validate_gpio_pin(value):
return value
def esp32_validate_supports(value):
def esp32_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
num = value[CONF_NUMBER]
mode = value[CONF_MODE]
is_input = mode[CONF_INPUT]
+3 -2
View File
@@ -1,4 +1,5 @@
import logging
from typing import Any
import esphome.config_validation as cv
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
@@ -9,14 +10,14 @@ _ESP32C2_STRAPPING_PINS = {8, 9}
_LOGGER = logging.getLogger(__name__)
def esp32_c2_validate_gpio_pin(value):
def esp32_c2_validate_gpio_pin(value: int) -> int:
if value < 0 or value > 20:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-20)")
return value
def esp32_c2_validate_supports(value):
def esp32_c2_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
num = value[CONF_NUMBER]
mode = value[CONF_MODE]
is_input = mode[CONF_INPUT]
+3 -2
View File
@@ -1,4 +1,5 @@
import logging
from typing import Any
import esphome.config_validation as cv
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
@@ -18,7 +19,7 @@ _ESP32C3_STRAPPING_PINS = {2, 8, 9}
_LOGGER = logging.getLogger(__name__)
def esp32_c3_validate_gpio_pin(value):
def esp32_c3_validate_gpio_pin(value: int) -> int:
if value < 0 or value > 21:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-21)")
if value in _ESP32C3_SPI_PSRAM_PINS:
@@ -29,7 +30,7 @@ def esp32_c3_validate_gpio_pin(value):
return value
def esp32_c3_validate_supports(value):
def esp32_c3_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
num = value[CONF_NUMBER]
mode = value[CONF_MODE]
is_input = mode[CONF_INPUT]
+3 -2
View File
@@ -1,4 +1,5 @@
import logging
from typing import Any
import esphome.config_validation as cv
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA
@@ -22,7 +23,7 @@ _ESP32C5_STRAPPING_PINS = {2, 7, 27, 28}
_LOGGER = logging.getLogger(__name__)
def esp32_c5_validate_gpio_pin(value):
def esp32_c5_validate_gpio_pin(value: int) -> int:
if value < 0 or value > 28:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-28)")
if value in _ESP32C5_SPI_PSRAM_PINS:
@@ -33,7 +34,7 @@ def esp32_c5_validate_gpio_pin(value):
return value
def esp32_c5_validate_supports(value):
def esp32_c5_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
num = value[CONF_NUMBER]
mode = value[CONF_MODE]
is_input = mode[CONF_INPUT]
+3 -2
View File
@@ -1,4 +1,5 @@
import logging
from typing import Any
import esphome.config_validation as cv
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA
@@ -22,7 +23,7 @@ _ESP32C6_STRAPPING_PINS = {8, 9, 15}
_LOGGER = logging.getLogger(__name__)
def esp32_c6_validate_gpio_pin(value):
def esp32_c6_validate_gpio_pin(value: int) -> int:
if value < 0 or value > 23:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-23)")
if value in _ESP32C6_SPI_PSRAM_PINS:
@@ -33,7 +34,7 @@ def esp32_c6_validate_gpio_pin(value):
return value
def esp32_c6_validate_supports(value):
def esp32_c6_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
num = value[CONF_NUMBER]
mode = value[CONF_MODE]
is_input = mode[CONF_INPUT]
+3 -2
View File
@@ -1,4 +1,5 @@
import logging
from typing import Any
import esphome.config_validation as cv
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
@@ -20,7 +21,7 @@ _ESP32C61_STRAPPING_PINS = {8, 9}
_LOGGER = logging.getLogger(__name__)
def esp32_c61_validate_gpio_pin(value):
def esp32_c61_validate_gpio_pin(value: int) -> int:
if value < 0 or value > 29:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-29)")
if value in _ESP32C61_SPI_PSRAM_PINS:
@@ -31,7 +32,7 @@ def esp32_c61_validate_gpio_pin(value):
return value
def esp32_c61_validate_supports(value):
def esp32_c61_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
num = value[CONF_NUMBER]
mode = value[CONF_MODE]
is_input = mode[CONF_INPUT]
+3 -2
View File
@@ -1,4 +1,5 @@
import logging
from typing import Any
import esphome.config_validation as cv
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
@@ -13,7 +14,7 @@ _ESP32H2_STRAPPING_PINS = {2, 3, 8, 9, 25}
_LOGGER = logging.getLogger(__name__)
def esp32_h2_validate_gpio_pin(value):
def esp32_h2_validate_gpio_pin(value: int) -> int:
if value < 0 or value > 27:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-27)")
if value in _ESP32H2_SPI_FLASH_PINS:
@@ -33,7 +34,7 @@ def esp32_h2_validate_gpio_pin(value):
return value
def esp32_h2_validate_supports(value):
def esp32_h2_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
num = value[CONF_NUMBER]
mode = value[CONF_MODE]
is_input = mode[CONF_INPUT]
+3 -2
View File
@@ -1,4 +1,5 @@
import logging
from typing import Any
import esphome.config_validation as cv
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA
@@ -14,7 +15,7 @@ _ESP32P4_STRAPPING_PINS = {34, 35, 36, 37, 38}
_LOGGER = logging.getLogger(__name__)
def esp32_p4_validate_gpio_pin(value):
def esp32_p4_validate_gpio_pin(value: int) -> int:
if value < 0 or value > 54:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-54)")
if value in _ESP32P4_USB_JTAG_PINS:
@@ -27,7 +28,7 @@ def esp32_p4_validate_gpio_pin(value):
return value
def esp32_p4_validate_supports(value):
def esp32_p4_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
num = value[CONF_NUMBER]
mode = value[CONF_MODE]
is_input = mode[CONF_INPUT]
+3 -2
View File
@@ -1,4 +1,5 @@
import logging
from typing import Any
import esphome.config_validation as cv
from esphome.const import (
@@ -26,7 +27,7 @@ _ESP32S2_STRAPPING_PINS = {0, 45, 46}
_LOGGER = logging.getLogger(__name__)
def esp32_s2_validate_gpio_pin(value):
def esp32_s2_validate_gpio_pin(value: int) -> int:
if value < 0 or value > 46:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-46)")
@@ -43,7 +44,7 @@ def esp32_s2_validate_gpio_pin(value):
return value
def esp32_s2_validate_supports(value):
def esp32_s2_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
num = value[CONF_NUMBER]
mode = value[CONF_MODE]
is_input = mode[CONF_INPUT]
+3 -2
View File
@@ -1,4 +1,5 @@
import logging
from typing import Any
import esphome.config_validation as cv
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
@@ -27,7 +28,7 @@ _ESP_32S3_STRAPPING_PINS = {0, 3, 45, 46}
_LOGGER = logging.getLogger(__name__)
def esp32_s3_validate_gpio_pin(value):
def esp32_s3_validate_gpio_pin(value: int) -> int:
if value < 0 or value > 48:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-48)")
@@ -49,7 +50,7 @@ def esp32_s3_validate_gpio_pin(value):
return value
def esp32_s3_validate_supports(value):
def esp32_s3_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
num = value[CONF_NUMBER]
mode = value[CONF_MODE]
is_input = mode[CONF_INPUT]
+6 -2
View File
@@ -604,11 +604,15 @@ async def ble_enabled_to_code(config, condition_id, template_arg, args):
return cg.new_Pvariable(condition_id, template_arg)
@automation.register_action("ble.enable", BLEEnableAction, cv.Schema({}))
@automation.register_action(
"ble.enable", BLEEnableAction, cv.Schema({}), synchronous=True
)
async def ble_enable_to_code(config, action_id, template_arg, args):
return cg.new_Pvariable(action_id, template_arg)
@automation.register_action("ble.disable", BLEDisableAction, cv.Schema({}))
@automation.register_action(
"ble.disable", BLEDisableAction, cv.Schema({}), synchronous=True
)
async def ble_disable_to_code(config, action_id, template_arg, args):
return cg.new_Pvariable(action_id, template_arg)
@@ -622,6 +622,7 @@ async def to_code(config):
),
validate_set_value_action,
),
synchronous=True,
)
async def ble_server_characteristic_set_value(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -641,6 +642,7 @@ async def ble_server_characteristic_set_value(config, action_id, template_arg, a
cv.Required(CONF_VALUE): value_schema(),
}
),
synchronous=True,
)
async def ble_server_descriptor_set_value(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -662,6 +664,7 @@ async def ble_server_descriptor_set_value(config, action_id, template_arg, args)
),
validate_notify_action,
),
synchronous=True,
)
async def ble_server_characteristic_notify(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -373,6 +373,7 @@ ESP32_BLE_START_SCAN_ACTION_SCHEMA = cv.Schema(
"esp32_ble_tracker.start_scan",
ESP32BLEStartScanAction,
ESP32_BLE_START_SCAN_ACTION_SCHEMA,
synchronous=True,
)
async def esp32_ble_tracker_start_scan_action_to_code(
config, action_id, template_arg, args
@@ -396,6 +397,7 @@ ESP32_BLE_STOP_SCAN_ACTION_SCHEMA = automation.maybe_simple_id(
"esp32_ble_tracker.stop_scan",
ESP32BLEStopScanAction,
ESP32_BLE_STOP_SCAN_ACTION_SCHEMA,
synchronous=True,
)
async def esp32_ble_tracker_stop_scan_action_to_code(
config, action_id, template_arg, args
+2 -2
View File
@@ -103,9 +103,9 @@ async def to_code(config):
framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
os.environ["ESP_IDF_VERSION"] = f"{framework_ver.major}.{framework_ver.minor}"
if framework_ver >= cv.Version(5, 5, 0):
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.3.2")
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.4.0")
esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.4")
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.11.5")
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.12.1")
else:
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.13.0")
esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0")
+1
View File
@@ -57,6 +57,7 @@ async def to_code(config) -> None:
cv.Required(CONF_FREQUENCY): cv.templatable(validate_frequency),
}
),
synchronous=True,
)
async def esp8266_set_frequency_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
+1
View File
@@ -129,6 +129,7 @@ def adjusted_ldo_id(value):
),
}
),
synchronous=True,
)
async def ldo_voltage_adjust_to_code(config, action_id, template_arg, args):
parent = await cg.get_variable(config[CONF_ID])
+5
View File
@@ -220,6 +220,7 @@ SEND_SCHEMA.add_extra(_validate_send_action)
"espnow.send",
SendAction,
SEND_SCHEMA,
synchronous=False,
)
@automation.register_action(
"espnow.broadcast",
@@ -232,6 +233,7 @@ SEND_SCHEMA.add_extra(_validate_send_action)
),
key=CONF_DATA,
),
synchronous=False,
)
async def send_action(
config: ConfigType,
@@ -271,6 +273,7 @@ async def send_action(
PEER_SCHEMA,
key=CONF_ADDRESS,
),
synchronous=True,
)
@automation.register_action(
"espnow.peer.delete",
@@ -279,6 +282,7 @@ async def send_action(
PEER_SCHEMA,
key=CONF_ADDRESS,
),
synchronous=True,
)
async def peer_action(
config: ConfigType,
@@ -303,6 +307,7 @@ async def peer_action(
},
key=CONF_CHANNEL,
),
synchronous=True,
)
async def channel_action(
config: ConfigType,
@@ -21,22 +21,6 @@
namespace esphome::ethernet {
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2)
// work around IDF compile issue on P4 https://github.com/espressif/esp-idf/pull/15637
#ifdef USE_ESP32_VARIANT_ESP32P4
#undef ETH_ESP32_EMAC_DEFAULT_CONFIG
#define ETH_ESP32_EMAC_DEFAULT_CONFIG() \
{ \
.smi_gpio = {.mdc_num = 31, .mdio_num = 52}, .interface = EMAC_DATA_INTERFACE_RMII, \
.clock_config = {.rmii = {.clock_mode = EMAC_CLK_EXT_IN, .clock_gpio = (emac_rmii_clock_gpio_t) 50}}, \
.dma_burst_len = ETH_DMA_BURST_LEN_32, .intr_priority = 0, \
.emac_dataif_gpio = \
{.rmii = {.tx_en_num = 49, .txd0_num = 34, .txd1_num = 35, .crs_dv_num = 28, .rxd0_num = 29, .rxd1_num = 30}}, \
.clock_config_out_in = {.rmii = {.clock_mode = EMAC_CLK_EXT_IN, .clock_gpio = (emac_rmii_clock_gpio_t) -1}}, \
}
#endif
#endif
static const char *const TAG = "ethernet";
// PHY register size for hex logging
@@ -162,7 +146,7 @@ void EthernetComponent::setup() {
phy_config.phy_addr = this->phy_addr_;
phy_config.reset_gpio_num = this->power_pin_;
eth_esp32_emac_config_t esp32_emac_config = ETH_ESP32_EMAC_DEFAULT_CONFIG();
eth_esp32_emac_config_t esp32_emac_config = eth_esp32_emac_default_config();
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
esp32_emac_config.smi_gpio.mdc_num = this->mdc_pin_;
esp32_emac_config.smi_gpio.mdio_num = this->mdio_pin_;
@@ -11,10 +11,15 @@
#include "esp_eth.h"
#include "esp_eth_mac.h"
#include "esp_eth_mac_esp.h"
#include "esp_netif.h"
#include "esp_mac.h"
#include "esp_idf_version.h"
#if CONFIG_ETH_USE_ESP32_EMAC
extern "C" eth_esp32_emac_config_t eth_esp32_emac_default_config(void);
#endif
namespace esphome::ethernet {
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
@@ -0,0 +1,10 @@
#include "esp_eth_mac_esp.h"
// ETH_ESP32_EMAC_DEFAULT_CONFIG() uses out-of-order designated initializers
// which are valid in C but not in C++. This wrapper allows C++ code to get
// the default config without replicating the macro's contents.
#if CONFIG_ETH_USE_ESP32_EMAC
eth_esp32_emac_config_t eth_esp32_emac_default_config(void) {
return (eth_esp32_emac_config_t) ETH_ESP32_EMAC_DEFAULT_CONFIG();
}
#endif
+3 -1
View File
@@ -129,7 +129,9 @@ TRIGGER_EVENT_SCHEMA = cv.Schema(
)
@automation.register_action("event.trigger", TriggerEventAction, TRIGGER_EVENT_SCHEMA)
@automation.register_action(
"event.trigger", TriggerEventAction, TRIGGER_EVENT_SCHEMA, synchronous=True
)
async def event_fire_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
+24 -4
View File
@@ -81,7 +81,10 @@ EzoPMPArbitraryCommandAction = ezo_pmp_ns.class_(
@automation.register_action(
"ezo_pmp.find", EzoPMPFindAction, EZO_PMP_NO_ARGS_ACTION_SCHEMA
"ezo_pmp.find",
EzoPMPFindAction,
EZO_PMP_NO_ARGS_ACTION_SCHEMA,
synchronous=True,
)
async def ezo_pmp_find_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -92,6 +95,7 @@ async def ezo_pmp_find_to_code(config, action_id, template_arg, args):
"ezo_pmp.dose_continuously",
EzoPMPDoseContinuouslyAction,
EZO_PMP_NO_ARGS_ACTION_SCHEMA,
synchronous=True,
)
async def ezo_pmp_dose_continuously_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -102,6 +106,7 @@ async def ezo_pmp_dose_continuously_to_code(config, action_id, template_arg, arg
"ezo_pmp.clear_total_volume_dosed",
EzoPMPClearTotalVolumeDispensedAction,
EZO_PMP_NO_ARGS_ACTION_SCHEMA,
synchronous=True,
)
async def ezo_pmp_clear_total_volume_dosed_to_code(
config, action_id, template_arg, args
@@ -114,6 +119,7 @@ async def ezo_pmp_clear_total_volume_dosed_to_code(
"ezo_pmp.clear_calibration",
EzoPMPClearCalibrationAction,
EZO_PMP_NO_ARGS_ACTION_SCHEMA,
synchronous=True,
)
async def ezo_pmp_clear_calibration_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -121,7 +127,10 @@ async def ezo_pmp_clear_calibration_to_code(config, action_id, template_arg, arg
@automation.register_action(
"ezo_pmp.pause_dosing", EzoPMPPauseDosingAction, EZO_PMP_NO_ARGS_ACTION_SCHEMA
"ezo_pmp.pause_dosing",
EzoPMPPauseDosingAction,
EZO_PMP_NO_ARGS_ACTION_SCHEMA,
synchronous=True,
)
async def ezo_pmp_pause_dosing_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -129,7 +138,10 @@ async def ezo_pmp_pause_dosing_to_code(config, action_id, template_arg, args):
@automation.register_action(
"ezo_pmp.stop_dosing", EzoPMPStopDosingAction, EZO_PMP_NO_ARGS_ACTION_SCHEMA
"ezo_pmp.stop_dosing",
EzoPMPStopDosingAction,
EZO_PMP_NO_ARGS_ACTION_SCHEMA,
synchronous=True,
)
async def ezo_pmp_stop_dosing_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -149,7 +161,10 @@ EZO_PMP_DOSE_VOLUME_ACTION_SCHEMA = cv.All(
@automation.register_action(
"ezo_pmp.dose_volume", EzoPMPDoseVolumeAction, EZO_PMP_DOSE_VOLUME_ACTION_SCHEMA
"ezo_pmp.dose_volume",
EzoPMPDoseVolumeAction,
EZO_PMP_DOSE_VOLUME_ACTION_SCHEMA,
synchronous=True,
)
async def ezo_pmp_dose_volume_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -178,6 +193,7 @@ EZO_PMP_DOSE_VOLUME_OVER_TIME_ACTION_SCHEMA = cv.All(
"ezo_pmp.dose_volume_over_time",
EzoPMPDoseVolumeOverTimeAction,
EZO_PMP_DOSE_VOLUME_OVER_TIME_ACTION_SCHEMA,
synchronous=True,
)
async def ezo_pmp_dose_volume_over_time_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -209,6 +225,7 @@ EZO_PMP_DOSE_WITH_CONSTANT_FLOW_RATE_ACTION_SCHEMA = cv.All(
"ezo_pmp.dose_with_constant_flow_rate",
EzoPMPDoseWithConstantFlowRateAction,
EZO_PMP_DOSE_WITH_CONSTANT_FLOW_RATE_ACTION_SCHEMA,
synchronous=True,
)
async def ezo_pmp_dose_with_constant_flow_rate_to_code(
config, action_id, template_arg, args
@@ -239,6 +256,7 @@ EZO_PMP_SET_CALIBRATION_VOLUME_ACTION_SCHEMA = cv.All(
"ezo_pmp.set_calibration_volume",
EzoPMPSetCalibrationVolumeAction,
EZO_PMP_SET_CALIBRATION_VOLUME_ACTION_SCHEMA,
synchronous=True,
)
async def ezo_pmp_set_calibration_volume_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -262,6 +280,7 @@ EZO_PMP_CHANGE_I2C_ADDRESS_ACTION_SCHEMA = cv.All(
"ezo_pmp.change_i2c_address",
EzoPMPChangeI2CAddressAction,
EZO_PMP_CHANGE_I2C_ADDRESS_ACTION_SCHEMA,
synchronous=True,
)
async def ezo_pmp_change_i2c_address_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -285,6 +304,7 @@ EZO_PMP_ARBITRARY_COMMAND_ACTION_SCHEMA = cv.All(
"ezo_pmp.arbitrary_command",
EzoPMPArbitraryCommandAction,
EZO_PMP_ARBITRARY_COMMAND_ACTION_SCHEMA,
synchronous=True,
)
async def ezo_pmp_arbitrary_command_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
+1
View File
@@ -365,6 +365,7 @@ async def fan_turn_on_to_code(config, action_id, template_arg, args):
cv.Optional(CONF_OFF_SPEED_CYCLE, default=True): cv.boolean,
}
),
synchronous=True,
)
async def fan_cycle_speed_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -261,6 +261,7 @@ async def to_code(config):
},
key=CONF_FINGER_ID,
),
synchronous=True,
)
async def fingerprint_grow_enroll_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
@@ -282,6 +283,7 @@ async def fingerprint_grow_enroll_to_code(config, action_id, template_arg, args)
cv.GenerateID(): cv.use_id(FingerprintGrowComponent),
}
),
synchronous=True,
)
async def fingerprint_grow_cancel_enroll_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
@@ -299,6 +301,7 @@ async def fingerprint_grow_cancel_enroll_to_code(config, action_id, template_arg
},
key=CONF_FINGER_ID,
),
synchronous=True,
)
async def fingerprint_grow_delete_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
@@ -317,6 +320,7 @@ async def fingerprint_grow_delete_to_code(config, action_id, template_arg, args)
cv.GenerateID(): cv.use_id(FingerprintGrowComponent),
}
),
synchronous=True,
)
async def fingerprint_grow_delete_all_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
@@ -337,6 +341,7 @@ FINGERPRINT_GROW_LED_CONTROL_ACTION_SCHEMA = cv.maybe_simple_value(
"fingerprint_grow.led_control",
LEDControlAction,
FINGERPRINT_GROW_LED_CONTROL_ACTION_SCHEMA,
synchronous=True,
)
async def fingerprint_grow_led_control_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
@@ -359,6 +364,7 @@ async def fingerprint_grow_led_control_to_code(config, action_id, template_arg,
cv.Required(CONF_COUNT): cv.templatable(cv.uint8_t),
}
),
synchronous=True,
)
async def fingerprint_grow_aura_led_control_to_code(
config, action_id, template_arg, args
@@ -72,6 +72,7 @@ async def to_code(config):
cv.Required(CONF_DIRECTION): cv.enum(DIRECTION_TYPE, upper=True),
}
),
synchronous=True,
)
async def grove_tb6612fng_run_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
@@ -96,6 +97,7 @@ async def grove_tb6612fng_run_to_code(config, action_id, template_arg, args):
cv.Required(CONF_CHANNEL): cv.templatable(cv.int_range(min=0, max=1)),
}
),
synchronous=True,
)
async def grove_tb6612fng_break_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
@@ -115,6 +117,7 @@ async def grove_tb6612fng_break_to_code(config, action_id, template_arg, args):
cv.Required(CONF_CHANNEL): cv.templatable(cv.int_range(min=0, max=1)),
}
),
synchronous=True,
)
async def grove_tb6612fng_stop_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
@@ -133,6 +136,7 @@ async def grove_tb6612fng_stop_to_code(config, action_id, template_arg, args):
cv.Required(CONF_ID): cv.use_id(GROVE_TB6612FNG),
}
),
synchronous=True,
)
async def grove_tb6612fng_standby_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
@@ -149,6 +153,7 @@ async def grove_tb6612fng_standby_to_code(config, action_id, template_arg, args)
cv.Required(CONF_ID): cv.use_id(GROVE_TB6612FNG),
}
),
synchronous=True,
)
async def grove_tb6612fng_no_standby_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
@@ -166,6 +171,7 @@ async def grove_tb6612fng_no_standby_to_code(config, action_id, template_arg, ar
cv.Required(CONF_ADDRESS): cv.i2c_address,
}
),
synchronous=True,
)
async def grove_tb6612fng_change_address_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
+40 -9
View File
@@ -319,10 +319,16 @@ HAIER_HON_BASE_ACTION_SCHEMA = automation.maybe_simple_id(
@automation.register_action(
"climate.haier.display_on", DisplayOnAction, HAIER_BASE_ACTION_SCHEMA
"climate.haier.display_on",
DisplayOnAction,
HAIER_BASE_ACTION_SCHEMA,
synchronous=True,
)
@automation.register_action(
"climate.haier.display_off", DisplayOffAction, HAIER_BASE_ACTION_SCHEMA
"climate.haier.display_off",
DisplayOffAction,
HAIER_BASE_ACTION_SCHEMA,
synchronous=True,
)
async def display_action_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -330,10 +336,16 @@ async def display_action_to_code(config, action_id, template_arg, args):
@automation.register_action(
"climate.haier.beeper_on", BeeperOnAction, HAIER_HON_BASE_ACTION_SCHEMA
"climate.haier.beeper_on",
BeeperOnAction,
HAIER_HON_BASE_ACTION_SCHEMA,
synchronous=True,
)
@automation.register_action(
"climate.haier.beeper_off", BeeperOffAction, HAIER_HON_BASE_ACTION_SCHEMA
"climate.haier.beeper_off",
BeeperOffAction,
HAIER_HON_BASE_ACTION_SCHEMA,
synchronous=True,
)
async def beeper_action_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -345,11 +357,13 @@ async def beeper_action_to_code(config, action_id, template_arg, args):
"climate.haier.start_self_cleaning",
StartSelfCleaningAction,
HAIER_HON_BASE_ACTION_SCHEMA,
synchronous=True,
)
@automation.register_action(
"climate.haier.start_steri_cleaning",
StartSteriCleaningAction,
HAIER_HON_BASE_ACTION_SCHEMA,
synchronous=True,
)
async def start_cleaning_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -368,6 +382,7 @@ async def start_cleaning_to_code(config, action_id, template_arg, args):
),
}
),
synchronous=True,
)
async def haier_set_vertical_airflow_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -391,6 +406,7 @@ async def haier_set_vertical_airflow_to_code(config, action_id, template_arg, ar
),
}
),
synchronous=True,
)
async def haier_set_horizontal_airflow_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -403,10 +419,16 @@ async def haier_set_horizontal_airflow_to_code(config, action_id, template_arg,
@automation.register_action(
"climate.haier.health_on", HealthOnAction, HAIER_BASE_ACTION_SCHEMA
"climate.haier.health_on",
HealthOnAction,
HAIER_BASE_ACTION_SCHEMA,
synchronous=True,
)
@automation.register_action(
"climate.haier.health_off", HealthOffAction, HAIER_BASE_ACTION_SCHEMA
"climate.haier.health_off",
HealthOffAction,
HAIER_BASE_ACTION_SCHEMA,
synchronous=True,
)
async def health_action_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -414,13 +436,22 @@ async def health_action_to_code(config, action_id, template_arg, args):
@automation.register_action(
"climate.haier.power_on", PowerOnAction, HAIER_BASE_ACTION_SCHEMA
"climate.haier.power_on",
PowerOnAction,
HAIER_BASE_ACTION_SCHEMA,
synchronous=True,
)
@automation.register_action(
"climate.haier.power_off", PowerOffAction, HAIER_BASE_ACTION_SCHEMA
"climate.haier.power_off",
PowerOffAction,
HAIER_BASE_ACTION_SCHEMA,
synchronous=True,
)
@automation.register_action(
"climate.haier.power_toggle", PowerToggleAction, HAIER_BASE_ACTION_SCHEMA
"climate.haier.power_toggle",
PowerToggleAction,
HAIER_BASE_ACTION_SCHEMA,
synchronous=True,
)
async def power_action_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
+2 -3
View File
@@ -1375,9 +1375,8 @@ void HonClimate::process_protocol_reset() {
bool HonClimate::should_get_big_data_() {
if (this->big_data_sensors_ > 0) {
static uint8_t counter = 0;
counter = (counter + 1) % 3;
return counter == 1;
this->big_data_counter_ = (this->big_data_counter_ + 1) % 3;
return this->big_data_counter_ == 1;
}
return false;
}
+1
View File
@@ -188,6 +188,7 @@ class HonClimate : public HaierClimateBase {
float active_alarm_count_{NAN};
std::chrono::steady_clock::time_point last_alarm_request_;
int big_data_sensors_{0};
uint8_t big_data_counter_{0};
esphome::optional<hon_protocol::VerticalSwingMode> current_vertical_swing_{};
esphome::optional<hon_protocol::HorizontalSwingMode> current_horizontal_swing_{};
HonSettings settings_{};
@@ -52,6 +52,7 @@ CONFIG_SCHEMA = (
"fan.hbridge.brake",
BrakeAction,
maybe_simple_id({cv.GenerateID(): cv.use_id(HBridgeFan)}),
synchronous=True,
)
async def fan_hbridge_brake_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
+4 -1
View File
@@ -68,7 +68,10 @@ CALIBRATION_ACTION_SCHEMA = cv.Schema(
@automation.register_action(
"hc8.calibrate", HC8CalibrateAction, CALIBRATION_ACTION_SCHEMA
"hc8.calibrate",
HC8CalibrateAction,
CALIBRATION_ACTION_SCHEMA,
synchronous=True,
)
async def hc8_calibration_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
+8 -2
View File
@@ -114,7 +114,10 @@ HDC302X_HEATER_ON_ACTION_SCHEMA = maybe_simple_id(
@automation.register_action(
"hdc302x.heater_on", HeaterOnAction, HDC302X_HEATER_ON_ACTION_SCHEMA
"hdc302x.heater_on",
HeaterOnAction,
HDC302X_HEATER_ON_ACTION_SCHEMA,
synchronous=True,
)
async def hdc302x_heater_on_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
@@ -127,7 +130,10 @@ async def hdc302x_heater_on_to_code(config, action_id, template_arg, args):
@automation.register_action(
"hdc302x.heater_off", HeaterOffAction, HDC302X_ACTION_SCHEMA
"hdc302x.heater_off",
HeaterOffAction,
HDC302X_ACTION_SCHEMA,
synchronous=True,
)
async def hdc302x_heater_off_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
+5
View File
@@ -170,6 +170,7 @@ async def to_code(config):
},
key=CONF_NAME,
),
synchronous=True,
)
async def hlk_fm22x_enroll_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
@@ -192,6 +193,7 @@ async def hlk_fm22x_enroll_to_code(config, action_id, template_arg, args):
},
key=CONF_FACE_ID,
),
synchronous=True,
)
async def hlk_fm22x_delete_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
@@ -210,6 +212,7 @@ async def hlk_fm22x_delete_to_code(config, action_id, template_arg, args):
cv.GenerateID(): cv.use_id(HlkFm22xComponent),
}
),
synchronous=True,
)
async def hlk_fm22x_delete_all_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
@@ -225,6 +228,7 @@ async def hlk_fm22x_delete_all_to_code(config, action_id, template_arg, args):
cv.GenerateID(): cv.use_id(HlkFm22xComponent),
}
),
synchronous=True,
)
async def hlk_fm22x_scan_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
@@ -240,6 +244,7 @@ async def hlk_fm22x_scan_to_code(config, action_id, template_arg, args):
cv.GenerateID(): cv.use_id(HlkFm22xComponent),
}
),
synchronous=True,
)
async def hlk_fm22x_reset_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
+12 -3
View File
@@ -279,13 +279,22 @@ HTTP_REQUEST_SEND_ACTION_SCHEMA = HTTP_REQUEST_ACTION_SCHEMA.extend(
@automation.register_action(
"http_request.get", HttpRequestSendAction, HTTP_REQUEST_GET_ACTION_SCHEMA
"http_request.get",
HttpRequestSendAction,
HTTP_REQUEST_GET_ACTION_SCHEMA,
synchronous=True,
)
@automation.register_action(
"http_request.post", HttpRequestSendAction, HTTP_REQUEST_POST_ACTION_SCHEMA
"http_request.post",
HttpRequestSendAction,
HTTP_REQUEST_POST_ACTION_SCHEMA,
synchronous=True,
)
@automation.register_action(
"http_request.send", HttpRequestSendAction, HTTP_REQUEST_SEND_ACTION_SCHEMA
"http_request.send",
HttpRequestSendAction,
HTTP_REQUEST_SEND_ACTION_SCHEMA,
synchronous=True,
)
async def http_request_action_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -70,6 +70,7 @@ OTA_HTTP_REQUEST_FLASH_ACTION_SCHEMA = cv.All(
"ota.http_request.flash",
OtaHttpRequestComponentFlashAction,
OTA_HTTP_REQUEST_FLASH_ACTION_SCHEMA,
synchronous=True,
)
async def ota_http_request_action_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
+2
View File
@@ -93,6 +93,7 @@ async def to_code(config):
},
key=CONF_LEVEL,
),
synchronous=True,
)
async def set_heater_level_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
@@ -112,6 +113,7 @@ async def set_heater_level_to_code(config, action_id, template_arg, args):
},
key=CONF_STATUS,
),
synchronous=True,
)
async def set_heater_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
+1
View File
@@ -652,6 +652,7 @@ async def to_code(config: ConfigType) -> None:
},
key=CONF_BRIGHTNESS,
),
synchronous=True,
)
async def hub75_set_brightness_to_code(
config: ConfigType,

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