Compare commits

..
44 changed files with 331 additions and 1503 deletions
-126
View File
@@ -1,126 +0,0 @@
"""Build-time scans of the user's C++ sources: lambdas, ``includes:`` files and
external components.
The only scan today finds scanf calls with float conversions, which decides
whether float scanf support (or, on ESP-IDF Bluetooth builds, the libc sscanf)
must stay linked. Other source heuristics belong here too.
"""
import logging
from pathlib import Path
import re
from esphome.config_helpers import iter_include_files
from esphome.const import (
CONF_ESPHOME,
CONF_INCLUDES,
CONF_INCLUDES_C,
SOURCE_FILE_EXTENSIONS,
)
from esphome.core import Lambda
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
# The string literal handed to a scanf family call as its format: an optional
# first argument (sscanf/fscanf; a literal or a call with its own commas is
# fine) then the literal.
_SCANF_FORMAT_RE = re.compile(
r'scanf\s*\((?:(?:"(?:[^"\\]|\\.)*"|\([^()]*\)|[^,;"()])*,)?\s*"((?:[^"\\]|\\.)*)"'
)
# Standard scanf float conversions %f %F %e %E %g %G %a %A with optional
# suppression, width and length; also the invalid %.2f users write by analogy
# with printf.
_SCANF_FLOAT_SPEC_RE = re.compile(r"%[*\d.]*[hlL]*[feEgGaAF]")
def source_uses_scanf_float(src: str) -> bool:
"""Heuristic: does C++ source call a scanf family function with a float conversion?"""
if "scanf" not in src:
return False
src = Lambda.comment_remover(src)
return any(
_SCANF_FLOAT_SPEC_RE.search(match.group(1))
for match in _SCANF_FORMAT_RE.finditer(src)
)
def lambdas_use_scanf_float(config: ConfigType) -> bool:
"""Check if any lambda in the config uses scanf with a float format specifier."""
stack: list = [config]
while stack:
obj = stack.pop()
if isinstance(obj, Lambda):
if source_uses_scanf_float(obj.value):
return True
elif isinstance(obj, dict):
stack.extend(obj.values())
elif isinstance(obj, list):
stack.extend(obj)
return False
def includes_use_scanf_float(config: ConfigType) -> bool:
"""Check if an ``includes:`` or ``includes_c:`` file uses scanf with a float format specifier."""
esphome_config = config.get(CONF_ESPHOME, {})
includes = esphome_config.get(CONF_INCLUDES, []) + esphome_config.get(
CONF_INCLUDES_C, []
)
for path, _ in iter_include_files(includes):
if path.suffix not in SOURCE_FILE_EXTENSIONS:
continue
if source_uses_scanf_float(path.read_text(encoding="utf-8", errors="replace")):
return True
return False
def external_components_use_scanf_float(config: ConfigType) -> bool:
"""Whether a component from outside the esphome package scans a float."""
from esphome.config import iter_components
from esphome.loader import CORE_COMPONENTS_PATH
package_root = CORE_COMPONENTS_PATH.parent
for _, component in iter_components(config):
package_dir = Path(component.module.__file__).resolve().parent
if package_dir.is_relative_to(package_root):
continue
for resource in component.resources:
src = (package_dir / resource.resource).read_text(
encoding="utf-8", errors="replace"
)
if source_uses_scanf_float(src):
return True
return False
def user_code_uses_scanf_float(config: ConfigType) -> bool:
"""Whether a lambda, an ``includes:`` file or an external component scans a float."""
return (
lambdas_use_scanf_float(config)
or includes_use_scanf_float(config)
or external_components_use_scanf_float(config)
)
def keep_float_scanf(
option: bool | None, config: ConfigType, flash_note: str, override_note: str
) -> bool:
"""Resolve a tri-state float-scanf option: unset means "only if user code scans a float"."""
if option:
return True
found = user_code_uses_scanf_float(config)
if option is None:
if found:
_LOGGER.warning(
"Lambda, include or external component uses scanf with a float format "
"specifier; keeping float scanf support (%s)",
flash_note,
)
return found
if found:
_LOGGER.warning(
"Float scanf support is disabled but a lambda, include or external "
"component uses scanf with a float format specifier; %s",
override_note,
)
return False
-2
View File
@@ -608,7 +608,6 @@ message ListEntitiesCoverResponse {
bool supports_stop = 12;
uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"];
bool supports_stop_tilt = 14;
bool supports_toggle = 15;
}
// Deprecated in API version 1.1
@@ -671,7 +670,6 @@ message CoverCommandRequest {
bool stop = 8;
uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"];
bool stop_tilt = 10;
bool toggle = 11;
}
// ==================== FAN ====================
@@ -555,7 +555,6 @@ uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *c
msg.supports_tilt = traits.get_supports_tilt();
msg.supports_stop = traits.get_supports_stop();
msg.supports_stop_tilt = traits.get_supports_stop_tilt();
msg.supports_toggle = traits.get_supports_toggle();
return fill_and_encode_entity_info_with_device_class(cover, msg, msg.device_class, conn, remaining_size);
}
void APIConnection::on_cover_command_request(const CoverCommandRequest &msg) {
@@ -568,8 +567,6 @@ void APIConnection::on_cover_command_request(const CoverCommandRequest &msg) {
call.set_command_stop();
if (msg.stop_tilt)
call.set_command_stop_tilt();
if (msg.toggle)
call.set_command_toggle();
call.perform();
}
#endif
-5
View File
@@ -484,7 +484,6 @@ uint8_t *ListEntitiesCoverResponse::encode_msg(const void *self, uint8_t *__rest
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 13, msg.device_id);
#endif
pos = ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 14, msg.supports_stop_tilt);
pos = ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 15, msg.supports_toggle);
return pos;
}
uint32_t ListEntitiesCoverResponse::calc_size_msg(const void *self) {
@@ -507,7 +506,6 @@ uint32_t ListEntitiesCoverResponse::calc_size_msg(const void *self) {
size += ProtoSize::calc_uint32(1, msg.device_id);
#endif
size += ProtoSize::calc_bool(1, msg.supports_stop_tilt);
size += ProtoSize::calc_bool(1, msg.supports_toggle);
return size;
}
uint8_t *CoverStateResponse::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
@@ -567,9 +565,6 @@ void CoverCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t *
case proto_tag(10, WIRE_TYPE_VARINT):
msg.stop_tilt = value.as_bool();
break;
case proto_tag(11, WIRE_TYPE_VARINT):
msg.toggle = value.as_bool();
break;
default:
break;
}
+2 -4
View File
@@ -911,7 +911,7 @@ class BinarySensorStateResponse final : public StateResponseProtoMessage {
class ListEntitiesCoverResponse final : public InfoResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 13;
static constexpr uint8_t ESTIMATED_SIZE = 61;
static constexpr uint8_t ESTIMATED_SIZE = 59;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("list_entities_cover_response"); }
#endif
@@ -921,7 +921,6 @@ class ListEntitiesCoverResponse final : public InfoResponseProtoMessage {
ProgmemStringRef device_class{};
bool supports_stop{false};
bool supports_stop_tilt{false};
bool supports_toggle{false};
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
@@ -959,7 +958,7 @@ class CoverStateResponse final : public StateResponseProtoMessage {
class CoverCommandRequest final : public CommandProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 30;
static constexpr uint8_t ESTIMATED_SIZE = 29;
static constexpr uint8_t ESTIMATED_SIZE = 27;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("cover_command_request"); }
#endif
@@ -969,7 +968,6 @@ class CoverCommandRequest final : public CommandProtoMessage {
float tilt{0.0f};
bool stop{false};
bool stop_tilt{false};
bool toggle{false};
void decode(const uint8_t *buffer, size_t length) {
ProtoDecodableMessage::decode_fields(this, buffer, length, &decode_field);
}
-2
View File
@@ -1246,7 +1246,6 @@ const char *ListEntitiesCoverResponse::dump_to(DumpBuffer &out) const {
dump_field(out, ESPHOME_PSTR("device_id"), this->device_id);
#endif
dump_field(out, ESPHOME_PSTR("supports_stop_tilt"), this->supports_stop_tilt);
dump_field(out, ESPHOME_PSTR("supports_toggle"), this->supports_toggle);
return out.c_str();
}
const char *CoverStateResponse::dump_to(DumpBuffer &out) const {
@@ -1272,7 +1271,6 @@ const char *CoverCommandRequest::dump_to(DumpBuffer &out) const {
dump_field(out, ESPHOME_PSTR("device_id"), this->device_id);
#endif
dump_field(out, ESPHOME_PSTR("stop_tilt"), this->stop_tilt);
dump_field(out, ESPHOME_PSTR("toggle"), this->toggle);
return out.c_str();
}
#endif
+10 -52
View File
@@ -10,13 +10,11 @@ import subprocess
from typing import Any
from esphome import yaml_util
from esphome.code_scan import keep_float_scanf
import esphome.codegen as cg
from esphome.components.const import (
CONF_ENABLE_OTA_DOWNGRADE_PROTECTION,
CONF_IGNORE_NOT_FOUND,
)
from esphome.config_helpers import filter_source_files_from_defines
import esphome.config_validation as cv
from esphome.const import (
CONF_ADVANCED,
@@ -206,10 +204,6 @@ PSRAM_XIP_VARIANTS = {
# neither, so the engine is already in the image and the wrap saves nothing.
ROM_VSNPRINTF_WITHOUT_VASPRINTF_VARIANTS = {VARIANT_ESP32C6}
# Variants whose ROM exports sscanf with the newlib format in use (C6 normal
# format, C2 nano format by IDF default), so the sscanf wrap saves nothing.
ROM_SSCANF_VARIANTS = {VARIANT_ESP32C2, VARIANT_ESP32C6}
# NVS encryption (HMAC peripheral scheme) is only available on variants that
# expose the HMAC peripheral (SOC_HMAC_SUPPORTED in soc_caps.h). The original
# ESP32 and ESP32-C2 do not have it. New variants with an HMAC peripheral
@@ -2058,7 +2052,6 @@ CONF_RINGBUF_IN_IRAM = "ringbuf_in_iram"
CONF_HEAP_IN_IRAM = "heap_in_iram"
CONF_LOOP_TASK_STACK_SIZE = "loop_task_stack_size"
CONF_USE_FULL_CERTIFICATE_BUNDLE = "use_full_certificate_bundle"
CONF_ENABLE_FULL_SCANF = "enable_full_scanf"
CONF_DISABLE_DEBUG_STUBS = "disable_debug_stubs"
CONF_DISABLE_OCD_AWARE = "disable_ocd_aware"
CONF_DISABLE_USB_SERIAL_JTAG_SECONDARY = "disable_usb_serial_jtag_secondary"
@@ -2354,7 +2347,6 @@ FRAMEWORK_SCHEMA = cv.Schema(
CONF_INCLUDE_BUILTIN_IDF_COMPONENTS, default=[]
): cv.ensure_list(cv.string_strict),
cv.Optional(CONF_ENABLE_FULL_PRINTF, default=False): cv.boolean,
cv.Optional(CONF_ENABLE_FULL_SCANF): cv.boolean,
cv.Optional(CONF_DISABLE_DEBUG_STUBS, default=True): cv.boolean,
cv.Optional(CONF_DISABLE_OCD_AWARE, default=True): cv.boolean,
cv.Optional(
@@ -2657,32 +2649,6 @@ async def _set_libc_picolibc_newlib_compat() -> None:
)
def _add_wrap_stub(symbol: str, define: str) -> None:
"""Emit a linker wrap for ``symbol`` served by a stub compiled under ``define``.
--undefined pulls the stub from libsrc.a, scanned before the IDF library that
references the symbol.
"""
cg.add_define(define)
cg.add_build_flag(f"-Wl,--wrap={symbol}")
cg.add_build_flag(f"-Wl,--undefined=__wrap_{symbol}")
@coroutine_with_priority(CoroPriority.FINAL)
async def _add_sscanf_stub(enable_full_scanf: bool | None) -> None:
"""Wrap sscanf when bluedroid is the only reason newlib's scanf engine links.
FINAL priority so every request_bluetooth() call has happened.
"""
if get_esp32_variant() in ROM_SSCANF_VARIANTS or not _network_sdkconfig().bluetooth:
return
if keep_float_scanf(
enable_full_scanf, CORE.config, "~13KB flash", "that call will abort the device"
):
return
_add_wrap_stub("sscanf", "USE_ESP32_SSCANF_STUB")
@coroutine_with_priority(CoroPriority.FINAL)
async def _apply_nvs_cache_in_psram(explicit: bool) -> None:
"""Keep the NVS cache in PSRAM unless NVS encryption is on, however it was enabled."""
@@ -3124,8 +3090,9 @@ async def to_code(config):
# saves nothing while costing ~170 B of shim. IDF 5.x defaults to
# newlib on every variant; IDF 6.0+ switches to picolibc on every
# variant.
uses_newlib = idf_version() < cv.Version(6, 0, 0)
if conf[CONF_ADVANCED][CONF_ENABLE_FULL_PRINTF] or not uses_newlib:
if conf[CONF_ADVANCED][CONF_ENABLE_FULL_PRINTF] or idf_version() >= cv.Version(
6, 0, 0
):
cg.add_define("USE_FULL_PRINTF")
else:
for symbol in ("vprintf", "printf", "fprintf", "vfprintf"):
@@ -3133,14 +3100,14 @@ async def to_code(config):
# esp_http_client calls vasprintf, which on the ESP32-C6 is the only
# reference to newlib's full printf engine (~20 KB: _svfprintf_r,
# _dtoa_r and their helpers); every other caller resolves to the
# ROM. See vasprintf_stubs.cpp.
# ROM. See vasprintf_stubs.cpp. The --undefined flag is needed
# because libsrc.a is scanned before the IDF libraries that
# reference the symbol, so the stub would otherwise never be pulled
# from the archive.
if variant in ROM_VSNPRINTF_WITHOUT_VASPRINTF_VARIANTS:
_add_wrap_stub("vasprintf", "USE_ESP32_VASPRINTF_STUB")
if uses_newlib:
# bluedroid's sscanf calls; see sscanf_stubs.cpp
CORE.add_job(
_add_sscanf_stub, conf[CONF_ADVANCED].get(CONF_ENABLE_FULL_SCANF)
)
cg.add_define("USE_ESP32_VASPRINTF_STUB")
cg.add_build_flag("-Wl,--wrap=vasprintf")
cg.add_build_flag("-Wl,--undefined=__wrap_vasprintf")
else:
cg.add_build_flag("-DUSE_ARDUINO")
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ARDUINO")
@@ -4130,12 +4097,3 @@ def process_stacktrace(config, line, backtrace_state):
_decode_pc(config, addr.group())
return backtrace_state
# Each stub only compiles under its define, so keep the sources out of other builds
FILTER_SOURCE_FILES = filter_source_files_from_defines(
{
"sscanf_stubs.cpp": "USE_ESP32_SSCANF_STUB",
"vasprintf_stubs.cpp": "USE_ESP32_VASPRINTF_STUB",
}
)
-43
View File
@@ -1,43 +0,0 @@
/*
* Linker wrap for sscanf on ESP-IDF with newlib.
*
* bluedroid's two "%02x" parses are the only sscanf callers in an ESPHome
* image and link newlib's whole scanf engine (~13 KB with _strtod_l). Routing
* them through core/sscanf_no_float.h drops it; floats would pull _strtod_l
* back, so those abort naming enable_full_scanf. See _add_sscanf_stub in
* esp32/__init__.py for when this is emitted.
*/
#include "esphome/core/defines.h"
#if defined(USE_ESP_IDF) && defined(USE_ESP32_SSCANF_STUB)
#include <cstdarg>
#include <cstdio>
#include "esp_system.h"
#include "esphome/core/sscanf_no_float.h"
namespace esphome::esp32 {}
// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
extern "C" {
int __wrap_sscanf(const char *str, const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
int result = esphome::vsscanf_no_float(str, fmt, ap);
va_end(ap);
if (result == esphome::SSCANF_UNSUPPORTED) {
char msg[160];
snprintf(msg, sizeof(msg),
"sscanf: unsupported conversion in \"%s\"; set enable_full_scanf: true in esp32 framework", fmt);
esp_system_abort(msg);
}
return result;
}
} // extern "C"
// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
#endif // USE_ESP_IDF && USE_ESP32_SSCANF_STUB
+43 -8
View File
@@ -6,7 +6,6 @@ import subprocess
from typing import Any
from esphome.build_helpers.native import native_backend
from esphome.code_scan import keep_float_scanf
import esphome.codegen as cg
from esphome.components import network
import esphome.config_validation as cv
@@ -27,7 +26,13 @@ from esphome.const import (
ThreadModel,
Toolchain,
)
from esphome.core import CORE, CoroPriority, EsphomeError, coroutine_with_priority
from esphome.core import (
CORE,
CoroPriority,
EsphomeError,
Lambda,
coroutine_with_priority,
)
from esphome.core.config import BOARD_MAX_LENGTH
from esphome.helpers import IS_MACOS, copy_file_if_changed
from esphome.platformio.toolchain import copy_ccache_script
@@ -62,6 +67,15 @@ from .const import (
from .gpio import PinInitialState, add_pin_initial_states_array
CONF_ENABLE_SCANF_FLOAT = "enable_scanf_float"
# Heuristically matches scanf/sscanf calls with float format specifiers.
# Standard scanf float conversions: %f %F %e %E %g %G %a %A
# With optional modifiers: %*f (suppression), %8f (width), %lf %Lf (length)
# Also matches non-standard patterns like %.2f as a heuristic — these are
# invalid in scanf but users may write them by analogy with printf.
# Uses [^;]*? to stay within a single statement, preventing false positives
# from e.g. sscanf(buf, "%d", &x); printf("%f", val);
_SCANF_FLOAT_RE = re.compile(r"scanf\s*\([^;]*?%[*\d.]*[hlL]*[feEgGaAF]")
CODEOWNERS = ["@esphome/core"]
DOMAIN = "esp8266"
_LOGGER = logging.getLogger(__name__)
@@ -69,6 +83,26 @@ AUTO_LOAD = ["preferences"]
IS_TARGET_PLATFORM = True
def lambdas_use_scanf_float(config: ConfigType) -> bool:
"""Check if any lambda in the config uses scanf with a float format specifier.
Comments are stripped before matching to avoid false positives from
commented-out code. The cost of a false positive is only ~8KB flash.
"""
stack: list = [config]
while stack:
obj = stack.pop()
if isinstance(obj, Lambda):
src = obj.comment_remover(obj.value)
if _SCANF_FLOAT_RE.search(src):
return True
elif isinstance(obj, dict):
stack.extend(obj.values())
elif isinstance(obj, list):
stack.extend(obj)
return False
def set_core_data(config: ConfigType) -> ConfigType:
CORE.data[KEY_ESP8266] = {}
CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_ESP8266
@@ -391,12 +425,13 @@ async def to_code(config: ConfigType) -> None:
cg.add_define("USE_ESP8266_CRASH_HANDLER")
cg.add_define("USE_CRASH_HANDLER")
enable_scanf_float = keep_float_scanf(
config.get(CONF_ENABLE_SCANF_FLOAT),
CORE.config,
"~8KB flash",
"that call will not parse",
)
enable_scanf_float = config.get(CONF_ENABLE_SCANF_FLOAT)
if enable_scanf_float is None and lambdas_use_scanf_float(CORE.config):
enable_scanf_float = True
_LOGGER.warning(
"Lambda uses scanf with a float format specifier; "
"enabling scanf float support (~8KB flash)"
)
# The native generator reads the same decision (KEY_SCANF_FLOAT)
CORE.data[KEY_ESP8266][KEY_SCANF_FLOAT] = bool(enable_scanf_float)
+37 -76
View File
@@ -9,6 +9,7 @@
#include "esphome/core/application.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/core/progmem.h"
#include <algorithm>
#include <cinttypes>
@@ -19,23 +20,6 @@ namespace esphome::ld2450 {
ESPHOME_LOG_TAG(TAG, "ld2450");
enum BaudRate : uint8_t {
BAUD_RATE_9600 = 1,
BAUD_RATE_19200 = 2,
BAUD_RATE_38400 = 3,
BAUD_RATE_57600 = 4,
BAUD_RATE_115200 = 5,
BAUD_RATE_230400 = 6,
BAUD_RATE_256000 = 7,
BAUD_RATE_460800 = 8
};
enum ZoneType : uint8_t {
ZONE_DISABLED = 0,
ZONE_DETECTION = 1,
ZONE_FILTER = 2,
};
enum PeriodicData : uint8_t {
TARGET_X = 4,
TARGET_Y = 6,
@@ -54,61 +38,20 @@ enum AckData : uint8_t {
COMMAND_STATUS = 7,
};
// Memory-efficient lookup tables
struct StringToUint8 {
const char *str;
const uint8_t value;
};
// Index + 1 is the BaudRate value; the select options use the same order
PROGMEM_STRING_TABLE(BaudRateStrings, "9600", "19200", "38400", "57600", "115200", "230400", "256000", "460800");
static_assert(BAUD_RATE_9600 == 1 && BAUD_RATE_460800 == BaudRateStrings::COUNT, "BaudRate is index + 1");
// Index is the Direction value
PROGMEM_STRING_TABLE(DirectionStrings, "Approaching", "Moving away", "Stationary", "NA");
static_assert(DIRECTION_APPROACHING == 0 && DIRECTION_MOVING_AWAY == 1 && DIRECTION_STATIONARY == 2 &&
DIRECTION_NA == 3,
"Direction is the table index");
// Index is the ZoneType value; the select options use the same order
PROGMEM_STRING_TABLE(ZoneTypeStrings, "Disabled", "Detection", "Filter");
static_assert(ZONE_DISABLED == 0 && ZONE_DETECTION == 1 && ZONE_FILTER == 2, "ZoneType is the table index");
struct Uint8ToString {
const uint8_t value;
const char *str;
};
constexpr StringToUint8 BAUD_RATES_BY_STR[] = {
{"9600", BAUD_RATE_9600}, {"19200", BAUD_RATE_19200}, {"38400", BAUD_RATE_38400},
{"57600", BAUD_RATE_57600}, {"115200", BAUD_RATE_115200}, {"230400", BAUD_RATE_230400},
{"256000", BAUD_RATE_256000}, {"460800", BAUD_RATE_460800},
};
constexpr Uint8ToString DIRECTION_BY_UINT[] = {
{DIRECTION_APPROACHING, "Approaching"},
{DIRECTION_MOVING_AWAY, "Moving away"},
{DIRECTION_STATIONARY, "Stationary"},
{DIRECTION_NA, "NA"},
};
constexpr Uint8ToString ZONE_TYPE_BY_UINT[] = {
{ZONE_DISABLED, "Disabled"},
{ZONE_DETECTION, "Detection"},
{ZONE_FILTER, "Filter"},
};
constexpr StringToUint8 ZONE_TYPE_BY_STR[] = {
{"Disabled", ZONE_DISABLED},
{"Detection", ZONE_DETECTION},
{"Filter", ZONE_FILTER},
};
// Baud rates in the same order as BAUD_RATES_BY_STR for index-based lookup
constexpr uint32_t BAUD_RATES[] = {9600, 19200, 38400, 57600, 115200, 230400, 256000, 460800};
// Helper functions for lookups
template<size_t N> uint8_t find_uint8(const StringToUint8 (&arr)[N], const std::string &str) {
for (const auto &entry : arr) {
if (str == entry.str)
return entry.value;
}
return 0xFF; // Not found
}
template<size_t N> const char *find_str(const Uint8ToString (&arr)[N], uint8_t value) {
for (const auto &entry : arr) {
if (value == entry.value)
return entry.str;
}
return ""; // Not found
}
// Baud rates in the same order as BaudRateStrings for index-based lookup
static constexpr uint32_t BAUD_RATES[] PROGMEM = {9600, 19200, 38400, 57600, 115200, 230400, 256000, 460800};
// LD2450 UART Serial Commands
static constexpr uint8_t CMD_ENABLE_CONF = 0xFF;
@@ -546,7 +489,12 @@ void LD2450Component::handle_periodic_data_() {
if (this->direction_dedup_[index].next(direction)) {
text_sensor::TextSensor *tsd = this->direction_text_sensors_[index];
if (tsd != nullptr) {
tsd->publish_state(find_str(ld2450::DIRECTION_BY_UINT, direction));
char direction_str[DirectionStrings::MAX_LENGTH + 1];
ESPHOME_strncpy_P(
direction_str,
reinterpret_cast<ESPHOME_PGM_P>(DirectionStrings::get_progmem_str(direction, DirectionStrings::LAST_INDEX)),
sizeof(direction_str));
tsd->publish_state(direction_str);
}
}
#endif
@@ -864,8 +812,15 @@ void LD2450Component::set_bluetooth(bool enable) {
// Set Baud rate
void LD2450Component::set_baud_rate(const char *state) {
const int16_t index = progmem_string_table_find<BaudRateStrings>(state, strlen(state));
if (index >= 0) {
this->set_baud_rate(static_cast<BaudRate>(index + 1));
}
}
void LD2450Component::set_baud_rate(BaudRate rate) {
this->set_config_mode_(true);
const uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00};
const uint8_t cmd_value[2] = {rate, 0x00};
this->send_command_(CMD_SET_BAUD_RATE, cmd_value, sizeof(cmd_value));
this->set_timeout(200, [this]() { this->restart_(); });
}
@@ -873,8 +828,14 @@ void LD2450Component::set_baud_rate(const char *state) {
// Set Zone Type - one of: Disabled, Detection, Filter
void LD2450Component::set_zone_type(const char *state) {
ESP_LOGV(TAG, "Set zone type: %s", state);
uint8_t zone_type = find_uint8(ZONE_TYPE_BY_STR, state);
this->zone_type_ = zone_type;
const int16_t index = progmem_string_table_find<ZoneTypeStrings>(state, strlen(state));
if (index >= 0) {
this->set_zone_type(static_cast<ZoneType>(index));
}
}
void LD2450Component::set_zone_type(ZoneType type) {
this->zone_type_ = type;
this->send_set_zone_command_();
}
@@ -882,7 +843,7 @@ void LD2450Component::set_zone_type(const char *state) {
void LD2450Component::publish_zone_type() {
#ifdef USE_SELECT
if (this->zone_type_select_ != nullptr) {
this->zone_type_select_->publish_state(find_str(ZONE_TYPE_BY_UINT, this->zone_type_));
this->zone_type_select_->publish_state(static_cast<size_t>(this->zone_type_));
}
#endif
}
+19
View File
@@ -55,6 +55,23 @@ enum Direction : uint8_t {
DIRECTION_UNDEFINED = 4,
};
enum BaudRate : uint8_t {
BAUD_RATE_9600 = 1,
BAUD_RATE_19200 = 2,
BAUD_RATE_38400 = 3,
BAUD_RATE_57600 = 4,
BAUD_RATE_115200 = 5,
BAUD_RATE_230400 = 6,
BAUD_RATE_256000 = 7,
BAUD_RATE_460800 = 8
};
enum ZoneType : uint8_t {
ZONE_DISABLED = 0,
ZONE_DETECTION = 1,
ZONE_FILTER = 2,
};
// Target coordinate struct
struct Target {
int16_t x;
@@ -121,7 +138,9 @@ class LD2450Component : public Component, public uart::UARTDevice {
void set_bluetooth(bool enable);
void set_multi_target(bool enable);
void set_baud_rate(const char *state);
void set_baud_rate(BaudRate rate);
void set_zone_type(const char *state);
void set_zone_type(ZoneType type);
void publish_zone_type();
void factory_reset();
#ifdef USE_TEXT_SENSOR
@@ -35,6 +35,7 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD2450_ID])
selects = select.sub_selects(config, parent=hub)
# Option lists follow the order of the C++ enums; the hub maps by index
await selects(
CONF_BAUD_RATE,
hub.set_baud_rate_select,
@@ -4,7 +4,7 @@ namespace esphome::ld2450 {
void BaudRateSelect::control(size_t index) {
this->publish_state(index);
this->parent_->set_baud_rate(this->option_at(index));
this->parent_->set_baud_rate(static_cast<BaudRate>(index + 1));
}
} // namespace esphome::ld2450
@@ -4,7 +4,7 @@ namespace esphome::ld2450 {
void ZoneTypeSelect::control(size_t index) {
this->publish_state(index);
this->parent_->set_zone_type(this->option_at(index));
this->parent_->set_zone_type(static_cast<ZoneType>(index));
}
} // namespace esphome::ld2450
+11 -3
View File
@@ -1,6 +1,7 @@
#pragma once
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include <memory>
@@ -24,10 +25,17 @@
namespace esphome::ld24xx {
// Helper to find index of value in constexpr array
template<size_t N> optional<size_t> find_index(const uint32_t (&arr)[N], uint32_t value) {
// Index of value in a constant table that may live in flash, or none
template<typename T, size_t N> optional<size_t> find_index(const T (&arr)[N], T value) {
static_assert(sizeof(T) == 1 || sizeof(T) == 4, "flash tables are read as bytes or aligned 32 bit words");
for (size_t i = 0; i < N; i++) {
if (arr[i] == value)
T entry;
if constexpr (sizeof(T) == 1) {
entry = progmem_read_byte(reinterpret_cast<const uint8_t *>(&arr[i]));
} else {
entry = arr[i]; // an aligned 32 bit read from flash is safe on ESP8266
}
if (entry == value)
return i;
}
return {};
+1 -19
View File
@@ -1,6 +1,4 @@
from collections.abc import Callable, Collection, Iterator
import logging
from pathlib import Path
from collections.abc import Callable, Collection
from esphome.const import (
CONF_LEVEL,
@@ -11,11 +9,8 @@ from esphome.const import (
PlatformFramework,
)
from esphome.core import CORE
from esphome.helpers import walk_files
from esphome.util import OrderedDict
_LOGGER = logging.getLogger(__name__)
# Pre-build lookup map from (platform, framework) tuples to PlatformFramework enum
_PLATFORM_FRAMEWORK_LOOKUP = {
(pf.value[0].value, pf.value[1].value): pf for pf in PlatformFramework
@@ -196,16 +191,3 @@ def get_logger_level() -> str:
logger_config = CORE.config[CONF_LOGGER]
return logger_config.get(CONF_LEVEL, "DEBUG")
def iter_include_files(includes: list[str]) -> Iterator[tuple[Path, Path]]:
"""Yield ``(path, basename)`` per local ``includes:`` file; directory members keep the directory prefix."""
for include in includes:
if include.startswith("<") and include.endswith(">"):
continue
path = CORE.relative_config_path(include)
if path.is_dir():
for file in walk_files(path):
yield file, file.relative_to(path.parent)
else:
yield path, Path(path.name)
+11 -18
View File
@@ -658,8 +658,6 @@ class EsphomeCore:
self.main_statements: list[Statement] = []
# A list of statements to insert in the global block (includes and global variables)
self.global_statements: list[Statement] = []
# Generated code emitted after the user includes, so it can use what they declare
self.global_statements_after_includes: list[Statement] = []
# A map of platformio libraries to add to the project (shortname: (name, version, repository))
self.platformio_libraries: dict[str, Library] = {}
# A set of build flags to set in the platformio project
@@ -730,7 +728,6 @@ class EsphomeCore:
self.variables = {}
self.main_statements = []
self.global_statements = []
self.global_statements_after_includes = []
self.platformio_libraries = {}
self.build_flags = set()
self.cmake_args = {}
@@ -1048,8 +1045,8 @@ class EsphomeCore:
except RuntimeError as e:
raise EsphomeError(str(e)) from e
@staticmethod
def _as_statement(expression) -> "Statement":
def add(self, expression, prepend=False) -> "Statement":
"""Add an expression or statement to the main setup() block."""
from esphome.cpp_generator import Expression, Statement, statement
if isinstance(expression, Expression):
@@ -1058,11 +1055,7 @@ class EsphomeCore:
raise ValueError(
f"Add '{expression}' must be expression or statement, not {type(expression)}"
)
return expression
def add(self, expression, prepend=False) -> "Statement":
"""Add an expression or statement to the main setup() block."""
expression = self._as_statement(expression)
if prepend:
self.main_statements.insert(0, expression)
else:
@@ -1071,7 +1064,14 @@ class EsphomeCore:
return expression
def add_global(self, expression, prepend=False) -> "Statement":
expression = self._as_statement(expression)
from esphome.cpp_generator import Expression, Statement, statement
if isinstance(expression, Expression):
expression = statement(expression)
if not isinstance(expression, Statement):
raise ValueError(
f"Add '{expression}' must be expression or statement, not {type(expression)}"
)
if prepend:
self.global_statements.insert(0, expression)
else:
@@ -1079,13 +1079,6 @@ class EsphomeCore:
_LOGGER.debug("Adding global: %s", expression)
return expression
def add_global_after_includes(self, expression) -> "Statement":
"""Add a global after the user includes, for generated code that may use them."""
expression = self._as_statement(expression)
self.global_statements_after_includes.append(expression)
_LOGGER.debug("Adding global after includes: %s", expression)
return expression
def add_library(self, library: Library):
if not isinstance(library, Library):
raise TypeError(
@@ -1265,7 +1258,7 @@ class EsphomeCore:
from esphome.cpp_generator import statement
global_code = []
for exp in (*self.global_statements, *self.global_statements_after_includes):
for exp in self.global_statements:
text = str(statement(exp))
text = text.rstrip()
global_code.append(text)
+14 -7
View File
@@ -10,7 +10,6 @@ import esphome.codegen as cg
from esphome.config_helpers import (
filter_source_files_from_defines,
filter_source_files_from_platform,
iter_include_files,
)
import esphome.config_validation as cv
from esphome.const import (
@@ -49,7 +48,6 @@ from esphome.const import (
PLATFORM_ESP32,
PLATFORM_HOST,
PLATFORM_RP2,
SOURCE_FILE_EXTENSIONS,
PlatformFramework,
__version__ as ESPHOME_VERSION,
)
@@ -66,6 +64,7 @@ from esphome.helpers import (
fnv1a_32bit_hash,
get_str_env,
get_usable_cpu_count,
walk_files,
)
from esphome.types import ConfigType
@@ -133,7 +132,7 @@ SHARED_DEVICE_RELATION_DEFINES: dict[str, str | None] = {
}
Area = cg.esphome_ns.class_("Area")
VALID_INCLUDE_EXTS = SOURCE_FILE_EXTENSIONS
VALID_INCLUDE_EXTS = {".h", ".hpp", ".tcc", ".ino", ".cpp", ".c"}
def validate_hostname(config):
@@ -591,10 +590,18 @@ async def add_arduino_global_workaround():
@coroutine_with_priority(CoroPriority.FINAL)
async def add_includes(includes: list[str], is_c_header: bool = False) -> None:
# After the id declarations, so the headers can use id(x); the generated function bodies
# and flash tables come later still, so their lambdas can use the headers.
for path, basename in iter_include_files(includes):
include_file(path, basename, is_c_header)
# Add includes at the very end, so that the included files can access global variables
for include in includes:
path = CORE.relative_config_path(include)
if path.is_dir():
# Directory, copy tree
for p in walk_files(path):
basename = p.relative_to(path.parent)
include_file(p, basename, is_c_header)
else:
# Copy file
basename = Path(path.name)
include_file(path, basename, is_c_header)
def _add_library_str(lib: str) -> None:
-1
View File
@@ -434,7 +434,6 @@
#ifdef USE_ESP32
#define USE_ESP32_CRASH_HANDLER
#define USE_ESP32_VASPRINTF_STUB
#define USE_ESP32_SSCANF_STUB
#define USE_MQTT_IDF_ENQUEUE
#define USE_MQTT_TLS
#define USE_MQTT_CERT_BUNDLE
-277
View File
@@ -1,277 +0,0 @@
#pragma once
// sscanf without floating-point conversions, backing the linker wrap in
// components/esp32/sscanf_stubs.cpp. Integer, character, string and scanset
// conversions behave like libc; anything else returns SSCANF_UNSUPPORTED.
// Header only so the host unit tests can cover it.
#include <cstdarg>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include "esphome/core/helpers.h"
namespace esphome {
/// Returned by vsscanf_no_float() for a conversion it does not implement.
static constexpr int SSCANF_UNSUPPORTED = -2;
namespace sscanf_no_float_internal {
enum class SscanfLength : uint8_t {
SSCANF_LENGTH_NONE,
SSCANF_LENGTH_HH,
SSCANF_LENGTH_H,
SSCANF_LENGTH_L,
SSCANF_LENGTH_LL,
SSCANF_LENGTH_Z,
SSCANF_LENGTH_T,
SSCANF_LENGTH_PTR, // %p
};
inline bool is_space(char c) { return c == ' ' || (c >= '\t' && c <= '\r'); }
// The unsigned bit pattern is what the signed conversions want as well
inline void store_int(void *dest, SscanfLength length, uint64_t value) {
switch (length) {
case SscanfLength::SSCANF_LENGTH_HH:
*static_cast<uint8_t *>(dest) = static_cast<uint8_t>(value);
break;
case SscanfLength::SSCANF_LENGTH_H:
*static_cast<uint16_t *>(dest) = static_cast<uint16_t>(value);
break;
case SscanfLength::SSCANF_LENGTH_L:
// %l is defined in terms of long, whatever its width on the target
*static_cast<unsigned long *>(dest) = static_cast<unsigned long>(value); // NOLINT(google-runtime-int)
break;
case SscanfLength::SSCANF_LENGTH_LL:
*static_cast<uint64_t *>(dest) = value;
break;
case SscanfLength::SSCANF_LENGTH_Z:
*static_cast<size_t *>(dest) = static_cast<size_t>(value);
break;
case SscanfLength::SSCANF_LENGTH_T:
*static_cast<ptrdiff_t *>(dest) = static_cast<ptrdiff_t>(value);
break;
case SscanfLength::SSCANF_LENGTH_PTR:
*static_cast<uintptr_t *>(dest) = static_cast<uintptr_t>(value);
break;
default:
*static_cast<unsigned *>(dest) = static_cast<unsigned>(value);
break;
}
}
struct Scanset {
uint8_t bits[32]{};
bool negate{false};
bool contains(char c) const {
auto u = static_cast<uint8_t>(c);
return ((this->bits[u >> 3] >> (u & 7)) & 1u) != this->negate;
}
};
// Parses "[set]" from just after the '[' and returns the position after ']'
inline const char *parse_scanset(const char *f, Scanset &set) {
if (*f == '^') {
set.negate = true;
f++;
}
// A ']' right after '[' or '[^' is a member, not the terminator.
bool first = true;
for (; *f != '\0' && (first || *f != ']'); f++) {
first = false;
auto lo = static_cast<uint8_t>(*f);
auto hi = lo;
if (f[1] == '-' && f[2] != ']' && f[2] != '\0') {
hi = static_cast<uint8_t>(f[2]);
f += 2;
}
for (unsigned c = lo; c <= hi; c++)
set.bits[c >> 3] |= static_cast<uint8_t>(1u << (c & 7));
}
return *f == ']' ? f + 1 : f;
}
// Radix of an integer conversion: 0 detects from the prefix (%i), 0xFF is unsupported
inline unsigned int_base(char conv) {
switch (conv) {
case 'd':
case 'u':
return 10;
case 'i':
return 0;
case 'o':
return 8;
case 'x':
case 'X':
case 'p':
return 16;
default:
return 0xFF;
}
}
} // namespace sscanf_no_float_internal
/// sscanf semantics for the integer, character, string and scanset
/// conversions; returns SSCANF_UNSUPPORTED on any other conversion.
inline int vsscanf_no_float(const char *str, const char *fmt, va_list ap) { // NOLINT(readability-non-const-parameter)
using namespace sscanf_no_float_internal;
const char *in = str;
int assigned = 0;
bool input_failure = false;
for (const char *f = fmt; *f != '\0'; f++) {
if (is_space(*f)) {
while (is_space(*in))
in++;
continue;
}
bool literal = *f != '%';
if (!literal && f[1] == '%') {
// "%%" matches one '%' with no whitespace skip, like newlib
f++;
literal = true;
}
if (literal) {
if (*in != *f) {
input_failure = *in == '\0';
break;
}
in++;
continue;
}
f++;
bool suppress = false;
if (*f == '*') {
suppress = true;
f++;
}
size_t width = 0;
while (*f >= '0' && *f <= '9') {
width = width * 10 + static_cast<size_t>(*f - '0');
f++;
}
if (width == 0)
width = SIZE_MAX;
auto length = SscanfLength::SSCANF_LENGTH_NONE;
switch (*f) {
case 'h':
f++;
if (*f == 'h') {
length = SscanfLength::SSCANF_LENGTH_HH;
f++;
} else {
length = SscanfLength::SSCANF_LENGTH_H;
}
break;
case 'l':
f++;
if (*f == 'l') {
length = SscanfLength::SSCANF_LENGTH_LL;
f++;
} else {
length = SscanfLength::SSCANF_LENGTH_L;
}
break;
case 'j':
length = SscanfLength::SSCANF_LENGTH_LL;
f++;
break;
case 'z':
length = SscanfLength::SSCANF_LENGTH_Z;
f++;
break;
case 't':
length = SscanfLength::SSCANF_LENGTH_T;
f++;
break;
default:
break;
}
const char conv = *f;
if (conv == '\0')
break;
if (conv == 'n') {
if (!suppress)
store_int(va_arg(ap, void *), length, static_cast<uint64_t>(in - str));
continue;
}
const bool string_conv = conv == 'c' || conv == 's' || conv == '[';
if (string_conv && length != SscanfLength::SSCANF_LENGTH_NONE)
return SSCANF_UNSUPPORTED; // wide characters
if (conv == 'p')
length = SscanfLength::SSCANF_LENGTH_PTR;
unsigned base = string_conv ? 0 : int_base(conv);
if (base == 0xFF)
return SSCANF_UNSUPPORTED;
if (conv != 'c' && conv != '[') {
while (is_space(*in))
in++;
}
if (*in == '\0') {
input_failure = true;
break;
}
if (!string_conv) {
const char *p = in;
size_t n = 0;
bool negative = false;
if (*p == '+' || *p == '-') {
negative = *p == '-';
p++;
n++;
}
if ((base == 0 || base == 16) && p[0] == '0' && (p[1] == 'x' || p[1] == 'X') && n + 2 < width &&
parse_hex_char(p[2]) != INVALID_HEX_CHAR) {
p += 2;
n += 2;
base = 16;
}
if (base == 0)
base = *p == '0' ? 8 : 10;
uint64_t value = 0;
bool any_digit = false;
for (uint8_t digit; n < width && (digit = parse_hex_char(*p)) < base; p++, n++) {
value = value * base + digit;
any_digit = true;
}
if (!any_digit)
break;
in = p;
if (!suppress) {
store_int(va_arg(ap, void *), length, negative ? 0 - value : value);
assigned++;
}
continue;
}
char *dest = suppress ? nullptr : va_arg(ap, char *);
Scanset set;
if (conv == '[')
f = parse_scanset(f + 1, set) - 1; // the loop's f++ steps past the ']'
// %c reads width characters (default 1); a short read still assigns, like newlib
const size_t limit = conv == 'c' && width == SIZE_MAX ? 1 : width;
size_t n = 0;
while (n < limit && in[n] != '\0' && (conv == 'c' || (conv == '[' ? set.contains(in[n]) : !is_space(in[n])))) {
if (dest != nullptr)
dest[n] = in[n];
n++;
}
if (conv != 'c') {
if (n == 0)
break;
if (dest != nullptr)
dest[n] = '\0';
}
in += n;
if (!suppress)
assigned++;
}
if (input_failure && assigned == 0)
return EOF;
return assigned;
}
} // namespace esphome
+10 -20
View File
@@ -536,10 +536,8 @@ def shared_progmem_array(
used |= {str(i) for i in CORE.variables}
used |= set(RESERVED_IDS) | CORE.loaded_integrations
id_ = ID(ensure_unique_string(name, used), is_declaration=True, type=type_)
# After the user includes, so a lambda in the table can use what their headers declare.
CORE.add_global_after_includes(
ProgmemAssignmentExpression(type_, id_, rhs, constexpr)
)
# Global, so any scope can use it; anything a lambda references is already declared.
CORE.add_global(ProgmemAssignmentExpression(type_, id_, rhs, constexpr))
array = MockObj(id_, ".")
CORE.register_variable(id_, array)
if share:
@@ -791,16 +789,15 @@ def static_function(
"""Emit ``static <return_type> <name>(parameters) { body }`` at global scope and return an
expression naming it, for use as a template argument or a function pointer.
The declaration goes with the other globals, so a template argument can name the function
at once; the body goes after the user includes, where every id and everything those
headers declare is in scope.
Every id the body names must already be declared, which holds when the statements were
rendered through ``get_variable`` or ``process_lambda``.
"""
signature = (
f"static {safe_exp(return_type)} {name}({ParameterListExpression(*parameters)})"
)
add_global(RawStatement(f"{signature};"))
add_global_after_includes(
RawStatement(f"{signature} {{\n{indent(chr(10).join(body))}\n}}")
params = ParameterListExpression(*parameters)
add_global(
RawStatement(
f"static {safe_exp(return_type)} {name}({params}) {{\n"
f"{indent(chr(10).join(body))}\n}}"
)
)
return RawExpression(name)
@@ -819,13 +816,6 @@ def add_global(expression: SafeExpType | Statement, prepend: bool = False):
CORE.add_global(expression, prepend)
def add_global_after_includes(expression: SafeExpType | Statement):
"""Add a global after the user's ``includes:`` headers, for generated code whose
lambdas may use what those headers declare.
"""
CORE.add_global_after_includes(expression)
def add_library(name: str, version: str | None, repository: str | None = None):
"""Add a library to the codegen library storage.
-2
View File
@@ -219,8 +219,6 @@ ESPHOME_H_EXCLUDE = {
Path(ENTITY_TYPES_H_TARGET),
# main.cpp includes it after defining esphome_controllers()
Path("esphome/core/controller_dispatch.h"),
# only the esp32 sscanf wrap includes it
Path("esphome/core/sscanf_no_float.h"),
# build machinery, not user API
Path("esphome/core/pch_prefix.h"),
}
@@ -1,9 +0,0 @@
esphome:
name: test
esp32:
variant: esp32c6
framework:
type: esp-idf
esp32_ble_tracker:
@@ -1,11 +0,0 @@
esphome:
name: test
esp32:
board: esp32dev
framework:
type: esp-idf
advanced:
enable_full_printf: true
esp32_ble_tracker:
@@ -1,11 +0,0 @@
esphome:
name: test
esp32:
board: esp32dev
framework:
type: esp-idf
advanced:
enable_full_scanf: true
esp32_ble_tracker:
@@ -1,17 +0,0 @@
esphome:
name: test
esp32:
board: esp32dev
framework:
type: esp-idf
esp32_ble_tracker:
sensor:
- platform: template
name: Parsed
lambda: |-
float value = 0;
sscanf("1.5", "%f", &value);
return value;
+11 -34
View File
@@ -2260,51 +2260,28 @@ def test_mbedtls_tls_zigbee_requires_extras(
assert mbedtls.tls_extras_required == _CCM_EXTRAS
_VASPRINTF_STUB_FLAGS = {"-Wl,--wrap=vasprintf", "-Wl,--undefined=__wrap_vasprintf"}
@pytest.mark.parametrize(
("config_file", "symbol", "expected"),
("config_file", "expected"),
[
# vasprintf: only where the ROM lacks vasprintf but has vsnprintf
pytest.param("vasprintf_stub_c6.yaml", "vasprintf", True, id="vasprintf_c6"),
pytest.param(
"vasprintf_stub_c6_full_printf.yaml",
"vasprintf",
False,
id="vasprintf_c6_full_printf",
),
pytest.param(
"exclusion_reincludes.yaml", "vasprintf", False, id="vasprintf_esp32"
),
# sscanf: only for Bluetooth builds on variants whose ROM lacks sscanf,
# and not when a lambda scans a float
pytest.param(
"exclusion_reincludes_wifi_ble.yaml", "sscanf", True, id="sscanf_esp32_ble"
),
pytest.param("sscanf_stub_ble_c6.yaml", "sscanf", False, id="sscanf_c6_rom"),
pytest.param(
"sscanf_stub_ble_full_scanf.yaml", "sscanf", False, id="sscanf_full_scanf"
),
pytest.param(
"sscanf_stub_ble_lambda.yaml", "sscanf", False, id="sscanf_lambda_float"
),
pytest.param(
"sscanf_stub_ble_full_printf.yaml", "sscanf", True, id="sscanf_full_printf"
),
pytest.param("exclusion_reincludes.yaml", "sscanf", False, id="sscanf_no_ble"),
pytest.param("vasprintf_stub_c6.yaml", True, id="c6"),
pytest.param("vasprintf_stub_c6_full_printf.yaml", False, id="c6_full_printf"),
pytest.param("exclusion_reincludes.yaml", False, id="esp32"),
],
)
def test_wrap_stub_emission(
def test_vasprintf_stub_only_on_rom_vsnprintf_variants(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
config_file: str,
symbol: str,
expected: bool,
) -> None:
"""A linker wrap stub is emitted as a define plus --wrap/--undefined flag pair."""
"""The vasprintf wrap is emitted only where the ROM lacks vasprintf but has vsnprintf."""
generate_main(component_config_path(config_file))
flags = {f"-Wl,--wrap={symbol}", f"-Wl,--undefined=__wrap_{symbol}"}
assert (CORE.build_flags >= flags) is expected
assert (CORE.build_flags >= _VASPRINTF_STUB_FLAGS) is expected
defines = {define.name for define in CORE.defines}
assert (f"USE_ESP32_{symbol.upper()}_STUB" in defines) is expected
assert ("USE_ESP32_VASPRINTF_STUB" in defines) is expected
@pytest.mark.parametrize(
@@ -1,250 +0,0 @@
#include <gtest/gtest.h>
#include <cstdarg>
#include <cstdint>
#include <cstdio>
#include "esphome/core/sscanf_no_float.h"
namespace esphome::core::testing {
static int sscanf_no_float(const char *str, const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
int result = vsscanf_no_float(str, fmt, ap);
va_end(ap);
return result;
}
// --- The two bluedroid call sites the wrap exists for ---
TEST(SscanfNoFloat, BluedroidHexByte) {
unsigned int value = 0;
EXPECT_EQ(sscanf_no_float("ab", "%02x", &value), 1);
EXPECT_EQ(value, 0xabu);
// Width stops after two digits even when more follow
EXPECT_EQ(sscanf_no_float("ab12", "%02x", &value), 1);
EXPECT_EQ(value, 0xabu);
}
TEST(SscanfNoFloat, BluedroidAddress) {
uint32_t a[6] = {0};
EXPECT_EQ(sscanf_no_float("AA:bb:0C:dd:ee:FF", "%02x:%02x:%02x:%02x:%02x:%02x", a, a + 1, a + 2, a + 3, a + 4, a + 5),
6);
EXPECT_EQ(a[0], 0xaau);
EXPECT_EQ(a[1], 0xbbu);
EXPECT_EQ(a[2], 0x0cu);
EXPECT_EQ(a[3], 0xddu);
EXPECT_EQ(a[4], 0xeeu);
EXPECT_EQ(a[5], 0xffu);
// Short input assigns what matched, like libc
EXPECT_EQ(sscanf_no_float("AA:bb:0C", "%02x:%02x:%02x:%02x:%02x:%02x", a, a + 1, a + 2, a + 3, a + 4, a + 5), 3);
}
TEST(SscanfNoFloat, WidthWithByteLength) {
uint8_t mac[2] = {0};
EXPECT_EQ(sscanf_no_float("AA:bb", "%02hhx:%02hhx", mac, mac + 1), 2);
EXPECT_EQ(mac[0], 0xaa);
EXPECT_EQ(mac[1], 0xbb);
// The width still bounds the digits when a length modifier is present
EXPECT_EQ(sscanf_no_float("abcd", "%02hhx", mac), 1);
EXPECT_EQ(mac[0], 0xab);
}
// --- Formats bluedroid's hands-free client uses: %hu with %n ---
TEST(SscanfNoFloat, ShortWithPosition) {
uint16_t h = 0, m = 0, s = 0;
int n1 = 0, n2 = 0, n3 = 0;
EXPECT_EQ(sscanf_no_float("6:30:15", "%hu%n:%hu%n:%hu%n", &h, &n1, &m, &n2, &s, &n3), 3);
EXPECT_EQ(h, 6);
EXPECT_EQ(m, 30);
EXPECT_EQ(s, 15);
EXPECT_EQ(n1, 1);
EXPECT_EQ(n2, 4);
EXPECT_EQ(n3, 7);
EXPECT_EQ(sscanf_no_float("M3.2.0", "M%hu%n.%hu%n.%hu%n", &h, &n1, &m, &n2, &s, &n3), 3);
EXPECT_EQ(sscanf_no_float("X3.2.0", "M%hu", &h), 0);
}
// --- Integer conversions ---
TEST(SscanfNoFloat, SignedDecimalSkipsLeadingSpace) {
int value = 0;
EXPECT_EQ(sscanf_no_float(" -42 rest", "%d", &value), 1);
EXPECT_EQ(value, -42);
EXPECT_EQ(sscanf_no_float("+7", "%u", &value), 1);
EXPECT_EQ(value, 7);
}
TEST(SscanfNoFloat, AutoBase) {
int value = 0;
EXPECT_EQ(sscanf_no_float("0x1F", "%i", &value), 1);
EXPECT_EQ(value, 31);
EXPECT_EQ(sscanf_no_float("017", "%i", &value), 1);
EXPECT_EQ(value, 15);
EXPECT_EQ(sscanf_no_float("0", "%i", &value), 1);
EXPECT_EQ(value, 0);
EXPECT_EQ(sscanf_no_float("-0x10", "%i", &value), 1);
EXPECT_EQ(value, -16);
}
TEST(SscanfNoFloat, OctalAndHexPrefix) {
unsigned int value = 0;
EXPECT_EQ(sscanf_no_float("17", "%o", &value), 1);
EXPECT_EQ(value, 15u);
EXPECT_EQ(sscanf_no_float("+0x10", "%x", &value), 1);
EXPECT_EQ(value, 16u);
// "0x" with no digit after it: only the 0 is consumed
EXPECT_EQ(sscanf_no_float("0xg", "%x", &value), 1);
EXPECT_EQ(value, 0u);
EXPECT_EQ(sscanf_no_float("zz", "%x", &value), 0);
}
TEST(SscanfNoFloat, WidthSplitsDigits) {
int a = 0;
unsigned int b = 0;
EXPECT_EQ(sscanf_no_float("12345", "%3d%d", &a, &b), 2);
EXPECT_EQ(a, 123);
EXPECT_EQ(b, 45u);
EXPECT_EQ(sscanf_no_float("ff", "%1x", &b), 1);
EXPECT_EQ(b, 15u);
}
TEST(SscanfNoFloat, LengthModifiers) {
unsigned char hh = 0;
int16_t h = 0;
long l = 0; // NOLINT(google-runtime-int) %ld is defined in terms of long
uint64_t ll = 0;
size_t z = 0;
ptrdiff_t t = 0;
EXPECT_EQ(sscanf_no_float("255", "%hhu", &hh), 1);
EXPECT_EQ(hh, 255);
EXPECT_EQ(sscanf_no_float("-2", "%hd", &h), 1);
EXPECT_EQ(h, -2);
EXPECT_EQ(sscanf_no_float("-1", "%ld", &l), 1);
EXPECT_EQ(l, -1L);
EXPECT_EQ(sscanf_no_float("18446744073709551615", "%llu", &ll), 1);
EXPECT_EQ(ll, UINT64_MAX);
EXPECT_EQ(sscanf_no_float("123", "%ju", &ll), 1);
EXPECT_EQ(ll, 123u);
EXPECT_EQ(sscanf_no_float("9", "%zu", &z), 1);
EXPECT_EQ(z, 9u);
EXPECT_EQ(sscanf_no_float("-9", "%td", &t), 1);
EXPECT_EQ(t, -9);
}
TEST(SscanfNoFloat, Pointer) {
void *p = nullptr;
EXPECT_EQ(sscanf_no_float("0x1234", "%p", &p), 1);
EXPECT_EQ(reinterpret_cast<uintptr_t>(p), 0x1234u);
EXPECT_EQ(sscanf_no_float("abcd rest", "%p", &p), 1);
EXPECT_EQ(reinterpret_cast<uintptr_t>(p), 0xabcdu);
}
TEST(SscanfNoFloat, Suppression) {
int value = 0;
EXPECT_EQ(sscanf_no_float("1 2 3", "%*d %d %*d", &value), 1);
EXPECT_EQ(value, 2);
}
// --- Character, string and scanset conversions ---
TEST(SscanfNoFloat, CharDoesNotSkipSpace) {
char c[3] = {0};
EXPECT_EQ(sscanf_no_float("xyz", "%c%c", c, c + 1), 2);
EXPECT_EQ(c[0], 'x');
EXPECT_EQ(c[1], 'y');
EXPECT_EQ(sscanf_no_float(" x", "%c", c), 1);
EXPECT_EQ(c[0], ' ');
}
TEST(SscanfNoFloat, CharWidthShortRead) {
char c[4] = {0};
EXPECT_EQ(sscanf_no_float("abc", "%2c", c), 1);
EXPECT_EQ(c[0], 'a');
EXPECT_EQ(c[1], 'b');
EXPECT_EQ(c[2], '\0');
// Fewer characters than the width still count as one assignment, like newlib
EXPECT_EQ(sscanf_no_float("ab", "%3c", c), 1);
EXPECT_EQ(sscanf_no_float("", "%c", c), EOF);
}
TEST(SscanfNoFloat, StringStopsAtSpaceAndWidth) {
char buf[16] = {0};
EXPECT_EQ(sscanf_no_float("hello world", "%5s", buf), 1);
EXPECT_STREQ(buf, "hello");
EXPECT_EQ(sscanf_no_float(" x", "%1s", buf), 1);
EXPECT_STREQ(buf, "x");
EXPECT_EQ(sscanf_no_float(" ", "%s", buf), EOF);
}
TEST(SscanfNoFloat, Scansets) {
char buf[16] = {0};
char rest[16] = {0};
EXPECT_EQ(sscanf_no_float("key=val", "%[^=]=%s", buf, rest), 2);
EXPECT_STREQ(buf, "key");
EXPECT_STREQ(rest, "val");
// ']' first is a member; '-' last is literal
EXPECT_EQ(sscanf_no_float("]]ab", "%[]]", buf), 1);
EXPECT_STREQ(buf, "]]");
EXPECT_EQ(sscanf_no_float("a-z]x", "%[a-]", buf), 1);
EXPECT_STREQ(buf, "a-");
EXPECT_EQ(sscanf_no_float("abc", "%[^]]", buf), 1);
EXPECT_STREQ(buf, "abc");
// No member matched is a matching failure
EXPECT_EQ(sscanf_no_float("123", "%[a-z]", buf), 0);
}
// --- Literals, whitespace and return value ---
TEST(SscanfNoFloat, LiteralsAndPercent) {
int value = 0;
EXPECT_EQ(sscanf_no_float("50%", "%d%%", &value), 1);
EXPECT_EQ(value, 50);
// Like newlib, %% does not skip whitespace; an explicit space in the format does
EXPECT_EQ(sscanf_no_float("50 % 7", "%d%%%d", &value, &value), 1);
EXPECT_EQ(sscanf_no_float("50 % 7", "%d %%%d", &value, &value), 2);
EXPECT_EQ(value, 7);
EXPECT_EQ(sscanf_no_float("1,2", "%d,%d", &value, &value), 2);
EXPECT_EQ(sscanf_no_float("1 ,2", "%d,%d", &value, &value), 1);
EXPECT_EQ(sscanf_no_float("1, 2", "%d,%d", &value, &value), 2);
EXPECT_EQ(sscanf_no_float("12\n34", "%d%d", &value, &value), 2);
}
TEST(SscanfNoFloat, PositionOnly) {
int n = 0;
EXPECT_EQ(sscanf_no_float("ab", "ab%n", &n), 0);
EXPECT_EQ(n, 2);
EXPECT_EQ(sscanf_no_float("", "%n", &n), 0);
EXPECT_EQ(n, 0);
}
TEST(SscanfNoFloat, EofVersusMatchingFailure) {
int value = 0;
EXPECT_EQ(sscanf_no_float("", "%d", &value), EOF);
EXPECT_EQ(sscanf_no_float(" ", "%d", &value), EOF);
EXPECT_EQ(sscanf_no_float("a", "ab"), EOF);
EXPECT_EQ(sscanf_no_float("abc", "%d", &value), 0);
EXPECT_EQ(sscanf_no_float("ac", "ab"), 0);
EXPECT_EQ(sscanf_no_float("12abc", "%d%d", &value, &value), 1);
EXPECT_EQ(sscanf_no_float("12", "%d%d", &value, &value), 1);
}
// --- Conversions the scanner refuses rather than misparses ---
TEST(SscanfNoFloat, UnsupportedConversions) {
float f = 0;
double d = 0;
wchar_t w[4] = {0};
EXPECT_EQ(sscanf_no_float("1.5", "%f", &f), SSCANF_UNSUPPORTED);
EXPECT_EQ(sscanf_no_float("1.5", "%lf", &d), SSCANF_UNSUPPORTED);
EXPECT_EQ(sscanf_no_float("1e3", "%g", &d), SSCANF_UNSUPPORTED);
EXPECT_EQ(sscanf_no_float("0x1p3", "%a", &d), SSCANF_UNSUPPORTED);
EXPECT_EQ(sscanf_no_float("abc", "%ls", w), SSCANF_UNSUPPORTED);
EXPECT_EQ(sscanf_no_float("abc", "%lc", w), SSCANF_UNSUPPORTED);
// Earlier conversions do not hide a later unsupported one
int value = 0;
EXPECT_EQ(sscanf_no_float("1 2.5", "%d %f", &value, &f), SSCANF_UNSUPPORTED);
}
} // namespace esphome::core::testing
+1
View File
@@ -30,6 +30,7 @@ class TestableLD2450 : public LD2450Component {
using LD2450Component::buffer_pos_;
using LD2450Component::readline_;
using LD2450Component::zone_config_;
using LD2450Component::zone_type_;
void feed(const std::vector<uint8_t> &data) {
for (uint8_t byte : data) {
@@ -0,0 +1,85 @@
#include <algorithm>
#include "common.h"
namespace esphome::ld2450::testing {
namespace {
class CapturingUARTComponent : public MockUARTComponent {
public:
void write_array(const uint8_t *data, size_t len) override {
this->written.insert(this->written.end(), data, data + len);
}
std::vector<uint8_t> written;
};
// Header, length 4, command 0xA1 (set baud rate): the rate byte and a zero follow.
constexpr uint8_t SET_BAUD_RATE_PREFIX[] = {0xFD, 0xFC, 0xFB, 0xFA, 0x04, 0x00, 0xA1, 0x00};
// Header, length 28, command 0xC2 (set zone): the zone type byte and a zero follow.
constexpr uint8_t SET_ZONE_PREFIX[] = {0xFD, 0xFC, 0xFB, 0xFA, 0x1C, 0x00, 0xC2, 0x00};
// The two payload bytes after the first frame with this prefix, or -1 when it was not written.
template<size_t N> std::pair<int, int> payload_after(const std::vector<uint8_t> &w, const uint8_t (&prefix)[N]) {
auto it = std::search(w.begin(), w.end(), std::begin(prefix), std::end(prefix));
if (it == w.end() || static_cast<size_t>(w.end() - it) < N + 2)
return {-1, -1};
return {*(it + N), *(it + N + 1)};
}
} // namespace
class LD2450SelectNamesTest : public ::testing::Test {
protected:
void SetUp() override { this->ld2450_.set_uart_parent(&this->uart_); }
CapturingUARTComponent uart_;
TestableLD2450 ld2450_;
};
TEST_F(LD2450SelectNamesTest, BaudRateNamesSendTheirDeviceValue) {
static constexpr const char *NAMES[] = {"9600", "19200", "38400", "57600", "115200", "230400", "256000", "460800"};
for (uint8_t i = 0; i < 8; i++) {
this->uart_.written.clear();
this->ld2450_.set_baud_rate(NAMES[i]);
const auto payload = payload_after(this->uart_.written, SET_BAUD_RATE_PREFIX);
EXPECT_EQ(payload.first, i + 1) << NAMES[i];
EXPECT_EQ(payload.second, 0) << NAMES[i];
}
}
TEST_F(LD2450SelectNamesTest, BaudRateEnumMatchesTheSelectOrder) {
this->ld2450_.set_baud_rate(BAUD_RATE_256000);
EXPECT_EQ(payload_after(this->uart_.written, SET_BAUD_RATE_PREFIX).first, 7);
}
TEST_F(LD2450SelectNamesTest, UnknownBaudRateNameWritesNothing) {
this->ld2450_.set_baud_rate("1200");
EXPECT_TRUE(this->uart_.written.empty());
}
TEST_F(LD2450SelectNamesTest, ZoneTypeNamesSetTheTypeAndSendIt) {
static constexpr const char *NAMES[] = {"Disabled", "Detection", "Filter"};
for (uint8_t i = 0; i < 3; i++) {
this->uart_.written.clear();
this->ld2450_.set_zone_type(NAMES[i]);
EXPECT_EQ(this->ld2450_.zone_type_, i) << NAMES[i];
const auto payload = payload_after(this->uart_.written, SET_ZONE_PREFIX);
EXPECT_EQ(payload.first, i) << NAMES[i];
EXPECT_EQ(payload.second, 0) << NAMES[i];
}
}
TEST_F(LD2450SelectNamesTest, ZoneTypeEnumMatchesTheSelectOrder) {
this->ld2450_.set_zone_type(ZONE_FILTER);
EXPECT_EQ(this->ld2450_.zone_type_, 2);
this->ld2450_.set_zone_type(ZONE_DETECTION);
EXPECT_EQ(this->ld2450_.zone_type_, 1);
}
TEST_F(LD2450SelectNamesTest, UnknownZoneTypeNameChangesNothing) {
this->ld2450_.set_zone_type(ZONE_DETECTION);
this->uart_.written.clear();
this->ld2450_.set_zone_type("Bogus");
EXPECT_EQ(this->ld2450_.zone_type_, 1);
EXPECT_TRUE(this->uart_.written.empty());
}
} // namespace esphome::ld2450::testing
@@ -409,10 +409,6 @@ cover:
id: template_cover_actions
position: 0.0
tilt: 0.0
toggle_action:
- cover.template.publish:
id: template_cover_actions
current_operation: IDLE
stop_action:
- cover.template.publish:
id: template_cover_actions
-4
View File
@@ -236,10 +236,6 @@ async def yaml_config(request: pytest.FixtureRequest, unused_tcp_port: int) -> s
external_components_path = str(FIXTURES_DIR / "external_components")
content = content.replace("EXTERNAL_COMPONENT_PATH", external_components_path)
# Replace the user includes path placeholder if present
if "INCLUDES_PATH" in content:
content = content.replace("INCLUDES_PATH", str(FIXTURES_DIR / "includes"))
if shared_name is not None:
# _compile verifies the marked test compiles this content unmodified
request.node._shared_yaml_content = content
@@ -1,26 +0,0 @@
esphome:
name: api-cover-toggle-test
host:
api:
logger:
level: DEBUG
cover:
- platform: template
name: "Toggle Cover"
id: toggle_cover
toggle_action:
- cover.template.publish:
id: toggle_cover
position: 0.5
stop_action:
- cover.template.publish:
id: toggle_cover
position: 0.25
- platform: template
name: "Plain Cover"
id: plain_cover
stop_action:
- cover.template.publish:
id: plain_cover
position: 0.25
@@ -1,6 +0,0 @@
#pragma once
static constexpr int HEADER_CONSTANT = 42;
// Names the global by its id, so the header must come after the id declarations.
inline int header_reads_global() { return header_value->value() * 2; }
@@ -1,27 +0,0 @@
esphome:
name: includes-lambda-order
includes:
- INCLUDES_PATH/lambda_order.h
on_boot:
# An apply action value is rendered at file scope.
- globals.set:
id: header_value
value: !lambda return HEADER_CONSTANT;
host:
api:
logger:
globals:
- id: header_value
type: int
initial_value: "0"
sensor:
- platform: template
name: Header Value
update_interval: 100ms
lambda: return header_reads_global();
filters:
# A lambda in a flash table.
- filter_out: !lambda return HEADER_CONSTANT - 1;
+5 -12
View File
@@ -1,7 +1,7 @@
"""Minimal plaintext native-api client over a raw socket.
Reads only when told to, so tests control when the TCP pipe backs up toward
the device; message types are counted and each frame is kept for inspection.
the device; payloads are skipped and only message types are counted.
"""
from __future__ import annotations
@@ -59,17 +59,15 @@ class FrameParser:
def __init__(self) -> None:
self._buf = bytearray()
self.frames: list[tuple[int, bytes]] = []
def feed(self, data: bytes) -> list[int]:
self._buf.extend(data)
types: list[int] = []
while (frame := self._try_parse()) is not None:
self.frames.append(frame)
types.append(frame[0])
while (msg_type := self._try_parse()) is not None:
types.append(msg_type)
return types
def _try_parse(self) -> tuple[int, bytes] | None:
def _try_parse(self) -> int | None:
buf = self._buf
if not buf:
return None
@@ -82,9 +80,8 @@ class FrameParser:
msg_type, pos = type_decoded
if len(buf) - pos < size:
return None
payload = bytes(buf[pos : pos + size])
del buf[: pos + size]
return msg_type, payload
return msg_type
class RawApiClient:
@@ -148,9 +145,5 @@ class RawApiClient:
await asyncio.wait_for(_read_loop(), timeout)
def frames(self, msg_type: int) -> list[bytes]:
"""Payloads of every frame of msg_type received so far."""
return [payload for kind, payload in self._parser.frames if kind == msg_type]
def close(self) -> None:
self._sock.close()
-129
View File
@@ -1,129 +0,0 @@
"""The native API advertises cover toggle support and runs the toggle action on request."""
from __future__ import annotations
import asyncio
from collections.abc import Callable
import struct
from aioesphomeapi import CoverInfo, CoverState, EntityState, api_pb2
import pytest
from .log_utils import LineWaiter
from .raw_api_client import MESSAGE_TYPE_OF, RawApiClient, decode_varint, encode_varint
from .state_utils import InitialStateHelper, StateWaiter, require_entity
from .types import APIClientConnectedFactory, RunCompiledFunction
COVER_COMMAND = MESSAGE_TYPE_OF[api_pb2.CoverCommandRequest]
LIST_COVER = MESSAGE_TYPE_OF[api_pb2.ListEntitiesCoverResponse]
LIST_DONE = MESSAGE_TYPE_OF[api_pb2.ListEntitiesDoneResponse]
WIRE_VARINT, WIRE_LENGTH, WIRE_FIXED32 = 0, 2, 5
# ListEntitiesCoverResponse fields; the installed client library does not know supports_toggle yet
FIELD_ENTITY_KEY, FIELD_SUPPORTS_TOGGLE = 2, 15
# CoverCommandRequest fields
FIELD_KEY, FIELD_STOP, FIELD_TOGGLE = 1, 8, 11
def tag(field: int, wire_type: int) -> bytes:
return encode_varint((field << 3) | wire_type)
def decode_fields(payload: bytes) -> dict[int, list[int | bytes]]:
"""Split a protobuf payload into field number -> raw values."""
fields: dict[int, list[int | bytes]] = {}
buf = bytearray(payload)
pos = 0
while pos < len(buf):
key, pos = decode_varint(buf, pos)
field, wire_type = key >> 3, key & 0x07
if wire_type == WIRE_VARINT:
value, pos = decode_varint(buf, pos)
elif wire_type == WIRE_LENGTH:
length, pos = decode_varint(buf, pos)
value = bytes(buf[pos : pos + length])
pos += length
elif wire_type == WIRE_FIXED32:
value = struct.unpack_from("<I", buf, pos)[0]
pos += 4
else:
raise AssertionError(f"unexpected wire type {wire_type} for field {field}")
fields.setdefault(field, []).append(value)
return fields
@pytest.mark.asyncio
async def test_api_cover_toggle(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
unused_tcp_port: int,
) -> None:
lines = LineWaiter()
async with (
run_compiled(yaml_config, line_callback=lines.callback),
api_client_connected() as client,
RawApiClient(unused_tcp_port) as raw,
):
entities, _ = await client.list_entities_services()
toggle_cover = require_entity(entities, "toggle_cover", CoverInfo, exact=True)
plain_cover = require_entity(entities, "plain_cover", CoverInfo, exact=True)
positions: dict[int, list[float]] = {toggle_cover.key: [], plain_cover.key: []}
waiter = StateWaiter()
def on_state(state: EntityState) -> None:
if isinstance(state, CoverState) and state.key in positions:
positions[state.key].append(state.position)
waiter.on_state(state)
def position_is(key: int, value: float) -> Callable[[EntityState], bool]:
return lambda s: (
isinstance(s, CoverState)
and s.key == key
and s.position == pytest.approx(value, abs=0.01)
)
initial = InitialStateHelper(entities)
client.subscribe_states(initial.on_state_wrapper(on_state))
await initial.wait_for_initial_states()
# Capability discovery: only the cover with a toggle_action advertises toggle
await raw.connect()
await raw.send_message(api_pb2.ListEntitiesRequest())
await raw.read_until_frame(LIST_DONE)
supports_toggle = {}
for payload in raw.frames(LIST_COVER):
fields = decode_fields(payload)
key = fields[FIELD_ENTITY_KEY][0]
supports_toggle[key] = bool(fields.get(FIELD_SUPPORTS_TOGGLE, [0])[0])
assert supports_toggle == {toggle_cover.key: True, plain_cover.key: False}
def key_field(key: int) -> bytes:
return tag(FIELD_KEY, WIRE_FIXED32) + struct.pack("<I", key)
toggle = tag(FIELD_TOGGLE, WIRE_VARINT) + b"\x01"
stop = tag(FIELD_STOP, WIRE_VARINT) + b"\x01"
# A toggle request runs the toggle action, which publishes 0.5
toggled = waiter.expect(
position_is(toggle_cover.key, 0.5), label="toggle action"
)
await raw.send_raw(COVER_COMMAND, key_field(toggle_cover.key) + toggle)
await toggled
# A cover without toggle_action drops the request with a warning; its state does not change
await raw.send_raw(COVER_COMMAND, key_field(plain_cover.key) + toggle)
await lines.wait_for("Plain Cover", "toggle unsupported", timeout=2.0)
await asyncio.sleep(0.5)
assert all(p == pytest.approx(1.0) for p in positions[plain_cover.key])
# Stop and toggle in one request: stop wins and the toggle action does not run
before = len(positions[toggle_cover.key])
stopped = waiter.expect(
position_is(toggle_cover.key, 0.25), label="stop action"
)
await raw.send_raw(COVER_COMMAND, key_field(toggle_cover.key) + stop + toggle)
await stopped
await asyncio.sleep(0.5)
after = positions[toggle_cover.key][before:]
assert all(p == pytest.approx(0.25, abs=0.01) for p in after), after
@@ -1,31 +0,0 @@
"""Lambdas rendered at file scope can use the user's includes, which can name ids."""
from __future__ import annotations
from aioesphomeapi import SensorInfo, SensorState
import pytest
from .state_utils import require_entity, wait_for_state
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_includes_lambda_order(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""The apply action and the sensor filter use the header, the header reads the global."""
async with run_compiled(yaml_config), api_client_connected() as client:
entities, _ = await client.list_entities_services()
sensor = require_entity(entities, "header_value", SensorInfo)
state = await wait_for_state(
client,
lambda s: (
isinstance(s, SensorState)
and s.key == sensor.key
and not s.missing_state
),
)
# globals.set stored HEADER_CONSTANT and the header read it back doubled.
assert state.state == 84.0
+58 -2
View File
@@ -8,8 +8,64 @@ from unittest.mock import MagicMock, patch
import pytest
from esphome.components import esp8266
from esphome.components.esp8266 import check_rosetta
from esphome.core import EsphomeError
from esphome.components.esp8266 import check_rosetta, lambdas_use_scanf_float
from esphome.core import EsphomeError, Lambda
from esphome.types import ConfigType
@pytest.mark.parametrize(
("src", "expected"),
[
# Basic float formats
('sscanf(buf, "%f", &v)', True),
('sscanf(buf, "%F", &v)', True),
('sscanf(buf, "%e", &v)', True),
('sscanf(buf, "%E", &v)', True),
('sscanf(buf, "%g", &v)', True),
('sscanf(buf, "%G", &v)', True),
('sscanf(buf, "%a", &v)', True),
('sscanf(buf, "%A", &v)', True),
# With modifiers
('sscanf(buf, "%lf", &v)', True),
('sscanf(buf, "%Lf", &v)', True),
('sscanf(buf, "%8lf", &v)', True),
('sscanf(buf, "%*f")', True),
('sscanf(buf, "%.2f", &v)', True),
# Mixed formats
('sscanf(buf, "%d,%f", &a, &b)', True),
# fscanf and std::sscanf
('fscanf(fp, "%f", &v)', True),
('std::sscanf(buf, "%f", &v)', True),
# Multi-line
('sscanf(buf,\n"%f", &v)', True),
# No float format
('sscanf(buf, "%d", &v)', False),
('sscanf(buf, "%s", s)', False),
# printf not scanf
('printf("%f", val)', False),
# %f in a different statement after scanf
('sscanf(buf, "%d", &x); printf("%f", val);', False),
# scanf %f in comment only
('// sscanf(buf, "%f", &v)\nsscanf(buf, "%d", &x)', False),
('/* sscanf(buf, "%f") */\nsscanf(buf, "%d", &x)', False),
],
)
def test_lambdas_use_scanf_float(src: str, expected: bool) -> None:
"""Test scanf float detection in lambda source."""
config: ConfigType = {"test": [Lambda(src)]}
assert lambdas_use_scanf_float(config) is expected
def test_lambdas_use_scanf_float_no_lambdas() -> None:
"""Test with config containing no lambdas."""
config: ConfigType = {"key": "value", "list": [1, 2]}
assert lambdas_use_scanf_float(config) is False
def test_lambdas_use_scanf_float_nested() -> None:
"""Test detection in deeply nested config."""
config: ConfigType = {"a": {"b": {"c": [Lambda('sscanf(buf, "%f", &v)')]}}}
assert lambdas_use_scanf_float(config) is True
@pytest.fixture
+1 -35
View File
@@ -26,7 +26,7 @@ from esphome.const import (
KEY_TARGET_PLATFORM,
Toolchain,
)
from esphome.core import CORE, ID, KEY_CONTROLLER_REGISTRY_CONTROLLERS, config
from esphome.core import CORE, KEY_CONTROLLER_REGISTRY_CONTROLLERS, config
from esphome.core.config import (
CONF_RELATION,
CONF_SUSPEND_LOOP,
@@ -1114,40 +1114,6 @@ async def test_add_includes_with_single_file(
assert any('#include "my_header.h"' in inc for inc in includes_added)
@pytest.mark.asyncio
async def test_add_includes_sit_between_declarations_and_generated_code(
tmp_path: Path, mock_copy_file_if_changed: Mock
) -> None:
"""A header can name an id, and generated functions and tables can use the header."""
CORE.config_path = tmp_path / "config.yaml"
CORE.build_path = tmp_path / "build"
CORE.build_path.mkdir(parents=True, exist_ok=True)
header = tmp_path / "my_header.h"
header.write_text("static constexpr int MY_CONSTANT = 42;")
CORE.config = {}
value = cg.new_Pvariable(ID("my_value", is_declaration=True, type=cg.int32))
cg.static_function(
"esphome__my_action__fn", cg.void, [], [f"*{value} = MY_CONSTANT;"]
)
cg.shared_progmem_array("my_table", cg.int32, [cg.RawExpression("MY_CONSTANT")])
await config.add_includes([str(header)])
# A job that runs after the includes still lands before the generated code.
cg.add_global(cg.RawStatement("static constexpr int FINAL_JOB = 1;"))
section = CORE.cpp_global_section
expected_order = [
"*const my_value",
"static void esphome__my_action__fn();",
'#include "my_header.h"',
"static constexpr int FINAL_JOB = 1;",
"static void esphome__my_action__fn() {",
"my_table[] PROGMEM",
]
positions = [section.index(text) for text in expected_order]
assert positions == sorted(positions), section
@pytest.mark.asyncio
@pytest.mark.skipif(os.name == "nt", reason="Unix-specific test")
async def test_add_includes_with_directory_unix(
+5 -24
View File
@@ -785,9 +785,8 @@ class MockCodegen(NamedTuple):
new_pvariable: MagicMock
register_parented: AsyncMock
add_global: MagicMock
add_global_after_includes: MagicMock
add: MagicMock
calls: MagicMock # the global emitters and new_pvariable attached, to check their order
calls: MagicMock # new_pvariable and add_global attached, to check their order
@pytest.fixture
@@ -849,9 +848,6 @@ def mock_cg() -> Generator[MockCodegen]:
"esphome.codegen.register_parented", new_callable=AsyncMock
) as register_parented,
patch("esphome.cpp_generator.add_global") as add_global,
patch(
"esphome.cpp_generator.add_global_after_includes"
) as add_global_after_includes,
patch("esphome.codegen.add") as add,
):
get_variable.return_value = PARENT_OBJ
@@ -859,15 +855,8 @@ def mock_cg() -> Generator[MockCodegen]:
calls = MagicMock()
calls.attach_mock(new_pvariable, "new_pvariable")
calls.attach_mock(add_global, "add_global")
calls.attach_mock(add_global_after_includes, "add_global_after_includes")
yield MockCodegen(
get_variable,
new_pvariable,
register_parented,
add_global,
add_global_after_includes,
add,
calls,
get_variable, new_pvariable, register_parented, add_global, add, calls
)
@@ -995,7 +984,7 @@ async def _run_apply_condition(
def _apply_definition(mock_cg: MockCodegen) -> str:
return str(mock_cg.add_global_after_includes.call_args.args[0])
return str(mock_cg.add_global.call_args.args[0])
@pytest.mark.asyncio
@@ -1009,16 +998,8 @@ async def test_register_apply_action_entry(
action_id, template_arg = mock_cg.new_pvariable.call_args.args
assert action_id == ID("obj_1")
assert str(template_arg) == "<esphome__obj_1__fn, int32_t>"
# The declaration must precede the storage line that names the function; the body
# goes after the user includes.
assert [c[0] for c in mock_cg.calls.mock_calls] == [
"add_global",
"add_global_after_includes",
"new_pvariable",
]
assert str(mock_cg.add_global.call_args.args[0]) == (
"static void esphome__obj_1__fn(const std::remove_cvref_t<int32_t> & x);"
)
# The definition must precede the storage line that names the function.
assert [c[0] for c in mock_cg.calls.mock_calls] == ["add_global", "new_pvariable"]
assert _apply_definition(mock_cg).startswith(
"static void esphome__obj_1__fn(const std::remove_cvref_t<int32_t> & x) {"
)
-162
View File
@@ -1,162 +0,0 @@
"""Unit tests for esphome.code_scan."""
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from esphome.code_scan import (
external_components_use_scanf_float,
includes_use_scanf_float,
keep_float_scanf,
lambdas_use_scanf_float,
user_code_uses_scanf_float,
)
from esphome.const import CONF_ESPHOME, CONF_INCLUDES, CONF_INCLUDES_C
from esphome.core import CORE, Lambda
from esphome.loader import CORE_COMPONENTS_PATH, FileResource
from esphome.types import ConfigType
@pytest.mark.parametrize(
("src", "expected"),
[
# Basic float formats
('sscanf(buf, "%f", &v)', True),
('sscanf(buf, "%F", &v)', True),
('sscanf(buf, "%e", &v)', True),
('sscanf(buf, "%E", &v)', True),
('sscanf(buf, "%g", &v)', True),
('sscanf(buf, "%G", &v)', True),
('sscanf(buf, "%a", &v)', True),
('sscanf(buf, "%A", &v)', True),
# With modifiers
('sscanf(buf, "%lf", &v)', True),
('sscanf(buf, "%Lf", &v)', True),
('sscanf(buf, "%8lf", &v)', True),
('sscanf(buf, "%*f")', True),
('sscanf(buf, "%.2f", &v)', True),
# Mixed formats, including a ';' inside the format string
('sscanf(buf, "%d,%f", &a, &b)', True),
('sscanf(buf, "%d;%f", &a, &b)', True),
('sscanf(buf, "a;b", &a); sscanf(buf, "%d", &b); printf("%f", v)', False),
# Escaped quotes, nested calls, a ')' inside the format, an unterminated call
('sscanf(buf, "\\"%f\\"", &v)', True),
('sscanf(f(x), "%f", &v)', True),
('sscanf(s.substr(0, 2).c_str(), "%f", &v)', True),
('sscanf(strtok(s, ","), "%f", &v)', True),
('sscanf(strtok(s, ","), "%d", &v)', False),
('sscanf(buf, "%d)", &a); g("%f")', False),
('sscanf(buf, "%d", &a; g("%f")', False),
('sscanf(buf, "%f"', True),
# fscanf and std::sscanf
('fscanf(fp, "%f", &v)', True),
('std::sscanf(buf, "%f", &v)', True),
# Multi-line
('sscanf(buf,\n"%f", &v)', True),
# No float format
('sscanf(buf, "%d", &v)', False),
('sscanf(buf, "%s", s)', False),
# printf not scanf
('printf("%f", val)', False),
# %f in a different statement after scanf
('sscanf(buf, "%d", &x); printf("%f", val);', False),
# scanf %f in comment only
('// sscanf(buf, "%f", &v)\nsscanf(buf, "%d", &x)', False),
('/* sscanf(buf, "%f") */\nsscanf(buf, "%d", &x)', False),
],
)
def test_lambdas_use_scanf_float(src: str, expected: bool) -> None:
"""Test scanf float detection in lambda source."""
config: ConfigType = {"test": [Lambda(src)]}
assert lambdas_use_scanf_float(config) is expected
def test_lambdas_use_scanf_float_no_lambdas() -> None:
"""Test with config containing no lambdas."""
config: ConfigType = {"key": "value", "list": [1, 2]}
assert lambdas_use_scanf_float(config) is False
def test_lambdas_use_scanf_float_nested() -> None:
"""Test detection in deeply nested config."""
config: ConfigType = {"a": {"b": {"c": [Lambda('sscanf(buf, "%f", &v)')]}}}
assert lambdas_use_scanf_float(config) is True
def test_includes_use_scanf_float(setup_core: Path) -> None:
"""A float scanf in an includes: or includes_c: file is found; comments and non-source files are ignored."""
(setup_core / "parse.h").write_text(
'static void f(const char *b) { float v; sscanf(b, "%f", &v); }'
)
(setup_core / "lib").mkdir()
(setup_core / "lib" / "int.h").write_text(
'static int g(const char *b) { int v; sscanf(b, "%d", &v); return v; }'
)
(setup_core / "lib" / "notes.txt").write_text('sscanf(b, "%f", &v)')
(setup_core / "commented.h").write_text(
'// sscanf(b, "%f", &v)\n/* sscanf(b, "%g", &v) */'
)
assert (
includes_use_scanf_float({CONF_ESPHOME: {CONF_INCLUDES: ["parse.h"]}}) is True
)
assert (
includes_use_scanf_float({CONF_ESPHOME: {CONF_INCLUDES_C: ["parse.h"]}}) is True
)
assert includes_use_scanf_float({CONF_ESPHOME: {CONF_INCLUDES: ["lib"]}}) is False
assert (
includes_use_scanf_float(
{CONF_ESPHOME: {CONF_INCLUDES: ["commented.h", "<cstdio>"]}}
)
is False
)
assert includes_use_scanf_float({}) is False
def test_keep_float_scanf() -> None:
"""Explicit values win; unset follows whether user code scans a float."""
float_config: ConfigType = {
CONF_ESPHOME: {"on_boot": [Lambda('sscanf(b, "%f", &v)')]}
}
int_config: ConfigType = {
CONF_ESPHOME: {"on_boot": [Lambda('sscanf(b, "%d", &v)')]}
}
assert keep_float_scanf(True, int_config, "x", "y") is True
assert keep_float_scanf(False, float_config, "x", "y") is False
assert keep_float_scanf(False, int_config, "x", "y") is False
assert keep_float_scanf(None, float_config, "x", "y") is True
assert keep_float_scanf(None, int_config, "x", "y") is False
def test_external_components_use_scanf_float(setup_core: Path) -> None:
"""Sources of components outside the esphome package are scanned; in-tree ones are skipped."""
def manifest(package_dir: Path, *sources: str) -> MagicMock:
m = MagicMock()
m.is_platform_component = False
m.module.__file__ = str(package_dir / "__init__.py")
m.resources = [FileResource("pkg", name) for name in sources]
return m
(setup_core / "ext_float").mkdir()
(setup_core / "ext_float" / "parse.cpp").write_text(
'void f(const char *b) { float v; sscanf(b, "%f", &v); }'
)
(setup_core / "ext_int").mkdir()
(setup_core / "ext_int" / "parse.cpp").write_text(
'int g(const char *b) { int v; sscanf(b, "%d", &v); return v; }'
)
lookup = {
"ext_float": manifest(setup_core / "ext_float", "parse.cpp"),
"ext_int": manifest(setup_core / "ext_int", "parse.cpp"),
"sensor": manifest(CORE_COMPONENTS_PATH / "sensor", "sensor.cpp"),
"esphome": manifest(CORE_COMPONENTS_PATH.parent / "core", "config.cpp"),
}
with patch("esphome.config.get_component", side_effect=lookup.get):
CORE.config = {"esphome": {}, "sensor": {}, "ext_int": {}}
assert external_components_use_scanf_float(CORE.config) is False
CORE.config = {"sensor": {}, "ext_float": {}}
assert external_components_use_scanf_float(CORE.config) is True
assert user_code_uses_scanf_float(CORE.config) is True
-14
View File
@@ -1,7 +1,6 @@
"""Unit tests for esphome.config_helpers module."""
from collections.abc import Callable
from pathlib import Path
from unittest.mock import patch
import pytest
@@ -13,7 +12,6 @@ from esphome.config_helpers import (
filter_source_files_from_platform,
frameworks_for_platforms,
get_logger_level,
iter_include_files,
)
from esphome.const import (
CONF_LEVEL,
@@ -211,15 +209,3 @@ def test_filter_source_files_from_platform_unknown_pair() -> None:
{"a.cpp": {PlatformFramework.ESP32_ARDUINO}}
)
assert func() == []
def test_iter_include_files(setup_core: Path) -> None:
"""Files yield their name, directory members keep the directory prefix, system headers are skipped."""
(setup_core / "one.h").write_text("")
(setup_core / "lib").mkdir()
(setup_core / "lib" / "two.h").write_text("")
files = {
basename.as_posix()
for _, basename in iter_include_files(["one.h", "lib", "<cstdio>"])
}
assert files == {"one.h", "lib/two.h"}
+4 -25
View File
@@ -866,8 +866,7 @@ class TestSharedProgmemArray:
c = cg.shared_progmem_array("table", ct.uint8, [4])
assert a is b
assert str(a) != str(c)
late = CORE.global_statements_after_includes
assert sum("PROGMEM" in str(st) for st in late) == 2
assert sum("PROGMEM" in str(st) for st in CORE.global_statements) == 2
assert not any("PROGMEM" in str(st) for st in CORE.main_statements)
def test_share_false_always_emits_a_new_array(self) -> None:
@@ -875,8 +874,7 @@ class TestSharedProgmemArray:
a = cg.shared_progmem_array("table", ct.uint8, [1], share=False)
b = cg.shared_progmem_array("table", ct.uint8, [1], share=False)
assert str(a) != str(b)
late = CORE.global_statements_after_includes
assert sum("PROGMEM" in str(st) for st in late) == 2
assert sum("PROGMEM" in str(st) for st in CORE.global_statements) == 2
def test_is_static_pointer_only_for_placement_new(self) -> None:
CORE.config = {}
@@ -912,9 +910,7 @@ class TestSharedProgmemArray:
CORE.config = {}
cg.shared_progmem_array("table", ct.uint8, [1], constexpr=False)
(statement,) = (
str(st)
for st in CORE.global_statements_after_includes
if "PROGMEM" in str(st)
str(st) for st in CORE.global_statements if "PROGMEM" in str(st)
)
assert statement.startswith(
"ESPHOME_FLASH_CONSTINIT static uint8_t const table[] PROGMEM = {1}"
@@ -952,7 +948,7 @@ class TestProgmemString:
assert a is b
assert str(a) == "progmem_str"
assert str(c) == "progmem_str_2"
assert [str(st) for st in CORE.global_statements_after_includes] == [
assert [str(st) for st in CORE.global_statements] == [
'static constexpr char progmem_str[] PROGMEM = "level";',
'static constexpr char progmem_str_2[] PROGMEM = "say \\042hi\\042";',
]
@@ -961,7 +957,6 @@ class TestProgmemString:
self._set_platform(PLATFORM_ESP32)
assert str(cg.shared_progmem_string('say "hi"')) == '"say \\042hi\\042"'
assert not CORE.global_statements
assert not CORE.global_statements_after_includes
def test_extern_progmem_array_is_an_externally_linked_global() -> None:
@@ -972,22 +967,6 @@ def test_extern_progmem_array_is_an_externally_linked_global() -> None:
]
def test_static_function_declares_early_and_defines_after_includes() -> None:
fn = cg.static_function("esphome__act__fn", ct.void, [(ct.int32, "x")], ["return;"])
assert str(fn) == "esphome__act__fn"
assert [str(st) for st in CORE.global_statements] == [
"static void esphome__act__fn(int32_t x);"
]
assert [str(st) for st in CORE.global_statements_after_includes] == [
"static void esphome__act__fn(int32_t x) {\n return;\n}"
]
def test_add_global_after_includes_rejects_non_statements() -> None:
with pytest.raises(ValueError, match="must be expression or statement"):
CORE.add_global_after_includes("not a statement")
class TestEnumOptions:
def test_class_enum_with_prefix(self) -> None:
mode = cg.MockObj("esphome::foo", "::").enum("Mode", is_class=True)