Compare commits

..

4 Commits

Author SHA1 Message Date
pre-commit-ci-lite[bot]
21507c570d [pre-commit.ci lite] apply automatic fixes 2026-01-15 02:29:46 +00:00
J. Nick Koston
5b6be2c8d9 Update esphome/components/modbus_controller/text_sensor/modbus_textsensor.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-14 16:28:26 -10:00
J. Nick Koston
66e80fe13b Update esphome/components/modbus_controller/modbus_controller.h
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-14 16:28:16 -10:00
J. Nick Koston
a50654ef4d [modbus_controller] Use stack buffers instead of str_sprintf/str_snprintf 2026-01-14 15:13:54 -10:00
12 changed files with 37 additions and 114 deletions

View File

@@ -665,10 +665,15 @@ async def write_image(config, all_frames=False):
if is_svg_file(path):
import resvg_py
resize = resize or (None, None)
image_data = resvg_py.svg_to_bytes(
svg_path=str(path), width=resize[0], height=resize[1], dpi=100
)
if resize:
width, height = resize
# resvg-py allows rendering by width/height directly
image_data = resvg_py.svg_to_bytes(
svg_path=str(path), width=int(width), height=int(height)
)
else:
# Default size
image_data = resvg_py.svg_to_bytes(svg_path=str(path))
# Convert bytes to Pillow Image
image = Image.open(io.BytesIO(image_data))

View File

@@ -413,7 +413,6 @@ class TextValidator(LValidator):
str_args = [str(x) for x in value[CONF_ARGS]]
arg_expr = cg.RawExpression(",".join(str_args))
format_str = cpp_string_escape(format_str)
# str_sprintf justified: user-defined format, can't optimize without permanent RAM cost
sprintf_str = f"str_sprintf({format_str}, {arg_expr}).c_str()"
if nanval := value.get(CONF_IF_NAN):
nanval = cpp_string_escape(nanval)

View File

@@ -65,10 +65,7 @@ std::string lv_event_code_name_for(uint8_t event_code) {
if (event_code < sizeof(EVENT_NAMES) / sizeof(EVENT_NAMES[0])) {
return EVENT_NAMES[event_code];
}
// max 4 bytes: "%u" with uint8_t (max 255, 3 digits) + null
char buf[4];
snprintf(buf, sizeof(buf), "%u", event_code);
return buf;
return str_sprintf("%2d", event_code);
}
static void rounder_cb(lv_disp_drv_t *disp_drv, lv_area_t *area) {

View File

@@ -285,8 +285,13 @@ class ServerRegister {
case SensorValueType::S_QWORD_R:
return std::to_string(value);
case SensorValueType::FP32_R:
case SensorValueType::FP32:
return str_sprintf("%.1f", bit_cast<float>(static_cast<uint32_t>(value)));
case SensorValueType::FP32: {
// max 48: float with %.1f can be up to 42 chars incl. null (3.4e38 → 38 integer digits + decimal point + 1
// decimal digit + optional sign)
char buf[48];
snprintf(buf, sizeof(buf), "%.1f", bit_cast<float>(static_cast<uint32_t>(value)));
return buf;
}
default:
return std::to_string(value);
}

View File

@@ -16,12 +16,20 @@ void ModbusTextSensor::parse_and_publish(const std::vector<uint8_t> &data) {
while ((items_left > 0) && index < data.size()) {
uint8_t b = data[index];
switch (this->encode_) {
case RawEncoding::HEXBYTES:
output_str += str_snprintf("%02x", 2, b);
case RawEncoding::HEXBYTES: {
// max 3: 2 hex digits + null
char hex_buf[3];
snprintf(hex_buf, sizeof(hex_buf), "%02x", b);
output_str += hex_buf;
break;
case RawEncoding::COMMA:
output_str += str_sprintf(index != this->offset ? ",%d" : "%d", b);
}
case RawEncoding::COMMA: {
// max 5: optional ','(1) + uint8(3) + null, for both ",%d" and "%d"
char dec_buf[5];
snprintf(dec_buf, sizeof(dec_buf), index != this->offset ? ",%d" : "%d", b);
output_str += dec_buf;
break;
}
case RawEncoding::ANSI:
if (b < 0x20)
break;

View File

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

View File

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

View File

@@ -199,22 +199,11 @@ std::string str_snake_case(const std::string &str) {
}
return result;
}
char *str_sanitize_to(char *buffer, size_t buffer_size, const char *str) {
if (buffer_size == 0) {
return buffer;
}
size_t i = 0;
while (*str && i < buffer_size - 1) {
buffer[i++] = to_sanitized_char(*str++);
}
buffer[i] = '\0';
return buffer;
}
std::string str_sanitize(const std::string &str) {
std::string result;
result.resize(str.size());
str_sanitize_to(&result[0], str.size() + 1, str.c_str());
std::string result = str;
for (char &c : result) {
c = to_sanitized_char(c);
}
return result;
}
std::string str_snprintf(const char *fmt, size_t len, ...) {

View File

@@ -545,25 +545,7 @@ std::string str_snake_case(const std::string &str);
constexpr char to_sanitized_char(char c) {
return (c == '-' || c == '_' || (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) ? c : '_';
}
/** Sanitize a string to buffer, keeping only alphanumerics, dashes, and underscores.
*
* @param buffer Output buffer to write to.
* @param buffer_size Size of the output buffer.
* @param str Input string to sanitize.
* @return Pointer to buffer.
*
* Buffer size needed: strlen(str) + 1.
*/
char *str_sanitize_to(char *buffer, size_t buffer_size, const char *str);
/// Sanitize a string to buffer. Automatically deduces buffer size.
template<size_t N> inline char *str_sanitize_to(char (&buffer)[N], const char *str) {
return str_sanitize_to(buffer, N, str);
}
/// Sanitizes the input string by removing all characters but alphanumerics, dashes and underscores.
/// @warning Allocates heap memory. Use str_sanitize_to() with a stack buffer instead.
std::string str_sanitize(const std::string &str);
/// Calculate FNV-1 hash of a string while applying snake_case + sanitize transformations.

View File

@@ -687,7 +687,6 @@ HEAP_ALLOCATING_HELPERS = {
"format_mac_address_pretty": "format_mac_addr_upper() with a stack buffer",
"get_mac_address": "get_mac_address_into_buffer() with a stack buffer",
"get_mac_address_pretty": "get_mac_address_pretty_into_buffer() with a stack buffer",
"str_sanitize": "str_sanitize_to() with a stack buffer",
"str_truncate": "removal (function is unused)",
"str_upper_case": "removal (function is unused)",
"str_snake_case": "removal (function is unused)",
@@ -705,7 +704,6 @@ HEAP_ALLOCATING_HELPERS = {
r"format_mac_address_pretty|"
r"get_mac_address_pretty(?!_)|"
r"get_mac_address(?!_)|"
r"str_sanitize(?!_)|"
r"str_truncate|"
r"str_upper_case|"
r"str_snake_case"

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="10mm" height="10mm" viewBox="0 0 100 100">
<rect x="0" y="0" width="100" height="100" fill="#00FF00"/>
<circle cx="50" cy="50" r="30" fill="#0000FF"/>
</svg>

Before

Width:  |  Height:  |  Size: 248 B

View File

@@ -5,21 +5,17 @@ from __future__ import annotations
from collections.abc import Callable
from pathlib import Path
from typing import Any
from unittest.mock import MagicMock, patch
import pytest
from esphome import config_validation as cv
from esphome.components.image import (
CONF_INVERT_ALPHA,
CONF_OPAQUE,
CONF_TRANSPARENCY,
CONFIG_SCHEMA,
get_all_image_metadata,
get_image_metadata,
write_image,
)
from esphome.const import CONF_DITHER, CONF_FILE, CONF_ID, CONF_RAW_DATA_ID, CONF_TYPE
from esphome.const import CONF_ID, CONF_RAW_DATA_ID, CONF_TYPE
from esphome.core import CORE
@@ -354,52 +350,3 @@ def test_get_all_image_metadata_empty() -> None:
"get_all_image_metadata should always return a dict"
)
# Length could be 0 or more depending on what's in CORE at test time
@pytest.fixture
def mock_progmem_array():
"""Mock progmem_array to avoid needing a proper ID object in tests."""
with patch("esphome.components.image.cg.progmem_array") as mock_progmem:
mock_progmem.return_value = MagicMock()
yield mock_progmem
@pytest.mark.asyncio
async def test_svg_with_mm_dimensions_succeeds(
component_config_path: Callable[[str], Path],
mock_progmem_array: MagicMock,
) -> None:
"""Test that SVG files with dimensions in mm are successfully processed."""
# Create a config for write_image without CONF_RESIZE
config = {
CONF_FILE: component_config_path("mm_dimensions.svg"),
CONF_TYPE: "BINARY",
CONF_TRANSPARENCY: CONF_OPAQUE,
CONF_DITHER: "NONE",
CONF_INVERT_ALPHA: False,
CONF_RAW_DATA_ID: "test_raw_data_id",
}
# This should succeed without raising an error
result = await write_image(config)
# Verify that write_image returns the expected tuple
assert isinstance(result, tuple), "write_image should return a tuple"
assert len(result) == 6, "write_image should return 6 values"
prog_arr, width, height, image_type, trans_value, frame_count = result
# Verify the dimensions are positive integers
# At 100 DPI, 10mm = ~39 pixels (10mm * 100dpi / 25.4mm_per_inch)
assert isinstance(width, int), "Width should be an integer"
assert isinstance(height, int), "Height should be an integer"
assert width > 0, "Width should be positive"
assert height > 0, "Height should be positive"
assert frame_count == 1, "Single image should have frame_count of 1"
# Verify we got reasonable dimensions from the mm-based SVG
assert 30 < width < 50, (
f"Width should be around 39 pixels for 10mm at 100dpi, got {width}"
)
assert 30 < height < 50, (
f"Height should be around 39 pixels for 10mm at 100dpi, got {height}"
)