Compare commits

...

8 Commits

Author SHA1 Message Date
J. Nick Koston
9d5be4d677 wip 2026-01-31 23:05:28 -06:00
J. Nick Koston
d4d55df878 _strtod_l 2026-01-31 19:32:25 -06:00
J. Nick Koston
6af53f1dec _strtod_l 2026-01-31 19:30:27 -06:00
J. Nick Koston
dd44e3a560 _strtod_l 2026-01-31 19:23:05 -06:00
J. Nick Koston
6e52b7dbcf _strtod_l 2026-01-31 19:15:47 -06:00
J. Nick Koston
d12e2fea3d _strtod_l 2026-01-31 19:12:30 -06:00
J. Nick Koston
dfaabfc98a _strtod_l 2026-01-31 18:59:43 -06:00
J. Nick Koston
0f21465646 _strtod_l 2026-01-31 18:48:40 -06:00
20 changed files with 236 additions and 84 deletions

View File

@@ -75,16 +75,16 @@ void ClimateCall::perform() {
ESP_LOGD(TAG, " Swing: %s", LOG_STR_ARG(swing_mode_s));
}
if (this->target_temperature_.has_value()) {
ESP_LOGD(TAG, " Target Temperature: %.2f", *this->target_temperature_);
ESP_LOGD(TAG, " Target Temperature: %s%d.%02d", DECIMAL_2(*this->target_temperature_));
}
if (this->target_temperature_low_.has_value()) {
ESP_LOGD(TAG, " Target Temperature Low: %.2f", *this->target_temperature_low_);
ESP_LOGD(TAG, " Target Temperature Low: %s%d.%02d", DECIMAL_2(*this->target_temperature_low_));
}
if (this->target_temperature_high_.has_value()) {
ESP_LOGD(TAG, " Target Temperature High: %.2f", *this->target_temperature_high_);
ESP_LOGD(TAG, " Target Temperature High: %s%d.%02d", DECIMAL_2(*this->target_temperature_high_));
}
if (this->target_humidity_.has_value()) {
ESP_LOGD(TAG, " Target Humidity: %.0f", *this->target_humidity_);
ESP_LOGD(TAG, " Target Humidity: %d%%", (int) *this->target_humidity_);
}
this->parent_->control(*this);
}
@@ -161,7 +161,8 @@ void ClimateCall::validate_() {
float low = *this->target_temperature_low_;
float high = *this->target_temperature_high_;
if (low > high) {
ESP_LOGW(TAG, " Target temperature low %.2f must be less than target temperature high %.2f", low, high);
ESP_LOGW(TAG, " Target temperature low %s%d.%02d must be less than high %s%d.%02d", DECIMAL_2(low),
DECIMAL_2(high));
this->target_temperature_low_.reset();
this->target_temperature_high_.reset();
}
@@ -458,20 +459,20 @@ void Climate::publish_state() {
ESP_LOGD(TAG, " Swing Mode: %s", LOG_STR_ARG(climate_swing_mode_to_string(this->swing_mode)));
}
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) {
ESP_LOGD(TAG, " Current Temperature: %.2f°C", this->current_temperature);
ESP_LOGD(TAG, " Current Temperature: %s%d.%02d°C", DECIMAL_2(this->current_temperature));
}
if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
ESP_LOGD(TAG, " Target Temperature: Low: %.2f°C High: %.2f°C", this->target_temperature_low,
this->target_temperature_high);
ESP_LOGD(TAG, " Target Temperature: Low: %s%d.%02d°C High: %s%d.%02d°C", DECIMAL_2(this->target_temperature_low),
DECIMAL_2(this->target_temperature_high));
} else {
ESP_LOGD(TAG, " Target Temperature: %.2f°C", this->target_temperature);
ESP_LOGD(TAG, " Target Temperature: %s%d.%02d°C", DECIMAL_2(this->target_temperature));
}
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) {
ESP_LOGD(TAG, " Current Humidity: %.0f%%", this->current_humidity);
ESP_LOGD(TAG, " Current Humidity: %d%%", (int) this->current_humidity);
}
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
ESP_LOGD(TAG, " Target Humidity: %.0f%%", this->target_humidity);
ESP_LOGD(TAG, " Target Humidity: %d%%", (int) this->target_humidity);
}
// Send state to frontend
@@ -720,21 +721,21 @@ void Climate::dump_traits_(const char *tag) {
ESP_LOGCONFIG(tag, "ClimateTraits:");
ESP_LOGCONFIG(tag,
" Visual settings:\n"
" - Min temperature: %.1f\n"
" - Max temperature: %.1f\n"
" - Min temperature: %s%d.%d\n"
" - Max temperature: %s%d.%d\n"
" - Temperature step:\n"
" Target: %.1f",
traits.get_visual_min_temperature(), traits.get_visual_max_temperature(),
traits.get_visual_target_temperature_step());
" Target: %s%d.%d",
DECIMAL_1(traits.get_visual_min_temperature()), DECIMAL_1(traits.get_visual_max_temperature()),
DECIMAL_1(traits.get_visual_target_temperature_step()));
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) {
ESP_LOGCONFIG(tag, " Current: %.1f", traits.get_visual_current_temperature_step());
ESP_LOGCONFIG(tag, " Current: %s%d.%d", DECIMAL_1(traits.get_visual_current_temperature_step()));
}
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY |
climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) {
ESP_LOGCONFIG(tag,
" - Min humidity: %.0f\n"
" - Max humidity: %.0f",
traits.get_visual_min_humidity(), traits.get_visual_max_humidity());
" - Min humidity: %d\n"
" - Max humidity: %d",
(int) traits.get_visual_min_humidity(), (int) traits.get_visual_max_humidity());
}
if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {

View File

@@ -79,13 +79,13 @@ void CoverCall::perform() {
}
if (this->position_.has_value()) {
if (traits.get_supports_position()) {
ESP_LOGD(TAG, " Position: %.0f%%", *this->position_ * 100.0f);
ESP_LOGD(TAG, " Position: %d%%", (int) (*this->position_ * 100.0f));
} else {
ESP_LOGD(TAG, " Command: %s", LOG_STR_ARG(cover_command_to_str(*this->position_)));
}
}
if (this->tilt_.has_value()) {
ESP_LOGD(TAG, " Tilt: %.0f%%", *this->tilt_ * 100.0f);
ESP_LOGD(TAG, " Tilt: %d%%", (int) (*this->tilt_ * 100.0f));
}
if (this->toggle_.has_value()) {
ESP_LOGD(TAG, " Command: TOGGLE");
@@ -105,7 +105,7 @@ void CoverCall::validate_() {
ESP_LOGW(TAG, "'%s': position unsupported", name);
this->position_.reset();
} else if (pos < 0.0f || pos > 1.0f) {
ESP_LOGW(TAG, "'%s': position %.2f out of range", name, pos);
ESP_LOGW(TAG, "'%s': position %s%d.%02d out of range", name, DECIMAL_2(pos));
this->position_ = clamp(pos, 0.0f, 1.0f);
}
}
@@ -115,7 +115,7 @@ void CoverCall::validate_() {
ESP_LOGW(TAG, "'%s': tilt unsupported", name);
this->tilt_.reset();
} else if (tilt < 0.0f || tilt > 1.0f) {
ESP_LOGW(TAG, "'%s': tilt %.2f out of range", name, tilt);
ESP_LOGW(TAG, "'%s': tilt %s%d.%02d out of range", name, DECIMAL_2(tilt));
this->tilt_ = clamp(tilt, 0.0f, 1.0f);
}
}
@@ -150,7 +150,7 @@ void Cover::publish_state(bool save) {
ESP_LOGD(TAG, "'%s' >>", this->name_.c_str());
auto traits = this->get_traits();
if (traits.get_supports_position()) {
ESP_LOGD(TAG, " Position: %.0f%%", this->position * 100.0f);
ESP_LOGD(TAG, " Position: %d%%", (int) (this->position * 100.0f));
} else {
if (this->position == COVER_OPEN) {
ESP_LOGD(TAG, " State: OPEN");
@@ -161,7 +161,7 @@ void Cover::publish_state(bool save) {
}
}
if (traits.get_supports_tilt()) {
ESP_LOGD(TAG, " Tilt: %.0f%%", this->tilt * 100.0f);
ESP_LOGD(TAG, " Tilt: %d%%", (int) (this->tilt * 100.0f));
}
ESP_LOGD(TAG, " Current Operation: %s", LOG_STR_ARG(cover_operation_to_str(this->current_operation)));

View File

@@ -205,6 +205,7 @@ async def to_code(config):
"pre:testing_mode.py",
"pre:exclude_updater.py",
"pre:exclude_waveform.py",
"pre:remove_float_printf.py",
"post:post_build.py",
],
)
@@ -342,3 +343,8 @@ def copy_files() -> None:
exclude_waveform_file,
CORE.relative_build_path("exclude_waveform.py"),
)
remove_float_printf_file = dir / "remove_float_printf.py.script"
copy_file_if_changed(
remove_float_printf_file,
CORE.relative_build_path("remove_float_printf.py"),
)

View File

@@ -0,0 +1,45 @@
# pylint: disable=E0602
Import("env") # noqa
# Remove float printf/scanf support from linker flags
# The Arduino ESP8266 framework unconditionally adds:
# -u _printf_float -u _scanf_float
# This forces inclusion of float formatting code (~7KB) even when not used.
#
# ESPHome avoids %f format specifiers in logging to not require this code.
# This script removes those flags to save flash space.
#
# Savings:
# - _dtoa_r: ~3.4KB (double-to-ASCII conversion)
# - _strtod_l: ~3.7KB (string-to-double conversion)
# - _printf_float: ~1.3KB
# - _scanf_float: ~1.3KB
# - Additional float math helpers
def remove_float_printf_flags(source, target, env):
"""Remove -u _printf_float and -u _scanf_float from linker flags.
This is called as a pre-action before the link step.
"""
linkflags = env.get("LINKFLAGS", [])
new_linkflags = []
i = 0
while i < len(linkflags):
flag = linkflags[i]
if flag == "-u" and i + 1 < len(linkflags):
next_flag = linkflags[i + 1]
if next_flag in ("_printf_float", "_scanf_float"):
print(f"ESPHome: Removing float printf support ({next_flag})")
i += 2 # Skip both -u and the symbol
continue
new_linkflags.append(flag)
i += 1
env.Replace(LINKFLAGS=new_linkflags)
# Register the callback to run before the link step
# This ensures it runs after the framework has added its flags
env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", remove_float_printf_flags)

View File

@@ -331,7 +331,7 @@ void ESPHomeOTAComponent::handle_data_() {
if (now - last_progress > 1000) {
last_progress = now;
float percentage = (total * 100.0f) / ota_size;
ESP_LOGD(TAG, "Progress: %0.1f%%", percentage);
ESP_LOGD(TAG, "Progress: %s%d.%d%%", DECIMAL_1(percentage));
#ifdef USE_OTA_STATE_LISTENER
this->notify_state_(ota::OTA_IN_PROGRESS, percentage, 0);
#endif

View File

@@ -169,14 +169,14 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
if ((now - last_progress > 1000) or (container->get_bytes_read() == container->content_length)) {
last_progress = now;
float percentage = container->get_bytes_read() * 100.0f / container->content_length;
ESP_LOGD(TAG, "Progress: %0.1f%%", percentage);
ESP_LOGD(TAG, "Progress: %s%d.%d%%", DECIMAL_1(percentage));
#ifdef USE_OTA_STATE_LISTENER
this->notify_state_(ota::OTA_IN_PROGRESS, percentage, 0);
#endif
}
} // while
ESP_LOGI(TAG, "Done in %.0f seconds", float(millis() - update_start_time) / 1000);
ESP_LOGI(TAG, "Done in %d seconds", (int) (float(millis() - update_start_time) / 1000));
// verify MD5 is as expected and act accordingly
md5_receive.calculate();

View File

@@ -13,7 +13,8 @@ static const char *const TAG = "light";
static void clamp_and_log_if_invalid(const char *name, float &value, const LogString *param_name, float min = 0.0f,
float max = 1.0f) {
if (value < min || value > max) {
ESP_LOGW(TAG, "'%s': %s value %.2f is out of range [%.1f - %.1f]", name, LOG_STR_ARG(param_name), value, min, max);
ESP_LOGW(TAG, "'%s': %s value %s%d.%02d is out of range [%s%d.%02d - %s%d.%02d]", name, LOG_STR_ARG(param_name),
DECIMAL_2(value), DECIMAL_2(min), DECIMAL_2(max));
value = clamp(value, min, max);
}
}
@@ -76,7 +77,7 @@ static const LogString *color_mode_to_human(ColorMode color_mode) {
// Helper to log percentage values
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG
static void log_percent(const LogString *param, float value) {
ESP_LOGD(TAG, " %s: %.0f%%", LOG_STR_ARG(param), value * 100.0f);
ESP_LOGD(TAG, " %s: %d%%", LOG_STR_ARG(param), (int) (value * 100.0f));
}
#else
#define log_percent(param, value)
@@ -112,34 +113,34 @@ void LightCall::perform() {
log_percent(LOG_STR("Color brightness"), v.get_color_brightness());
}
if (this->has_red() || this->has_green() || this->has_blue()) {
ESP_LOGD(TAG, " Red: %.0f%%, Green: %.0f%%, Blue: %.0f%%", v.get_red() * 100.0f, v.get_green() * 100.0f,
v.get_blue() * 100.0f);
ESP_LOGD(TAG, " Red: %d%%, Green: %d%%, Blue: %d%%", (int) (v.get_red() * 100.0f),
(int) (v.get_green() * 100.0f), (int) (v.get_blue() * 100.0f));
}
if (this->has_white()) {
log_percent(LOG_STR("White"), v.get_white());
}
if (this->has_color_temperature()) {
ESP_LOGD(TAG, " Color temperature: %.1f mireds", v.get_color_temperature());
ESP_LOGD(TAG, " Color temperature: %d mireds", (int) v.get_color_temperature());
}
if (this->has_cold_white() || this->has_warm_white()) {
ESP_LOGD(TAG, " Cold white: %.0f%%, warm white: %.0f%%", v.get_cold_white() * 100.0f,
v.get_warm_white() * 100.0f);
ESP_LOGD(TAG, " Cold white: %d%%, warm white: %d%%", (int) (v.get_cold_white() * 100.0f),
(int) (v.get_warm_white() * 100.0f));
}
}
if (this->has_flash_()) {
// FLASH
if (publish) {
ESP_LOGD(TAG, " Flash length: %.1fs", this->flash_length_ / 1e3f);
ESP_LOGD(TAG, " Flash length: %u ms", this->flash_length_);
}
this->parent_->start_flash_(v, this->flash_length_, publish);
} else if (this->has_transition_()) {
// TRANSITION
if (publish) {
ESP_LOGD(TAG, " Transition length: %.1fs", this->transition_length_ / 1e3f);
ESP_LOGD(TAG, " Transition length: %u ms", this->transition_length_);
}
// Special case: Transition and effect can be set when turning off

View File

@@ -92,15 +92,15 @@ void LightState::dump_config() {
auto traits = this->get_traits();
if (traits.supports_color_capability(ColorCapability::BRIGHTNESS)) {
ESP_LOGCONFIG(TAG,
" Default Transition Length: %.1fs\n"
" Gamma Correct: %.2f",
this->default_transition_length_ / 1e3f, this->gamma_correct_);
" Default Transition Length: %u ms\n"
" Gamma Correct: %s%d.%02d",
this->default_transition_length_, DECIMAL_2(this->gamma_correct_));
}
if (traits.supports_color_capability(ColorCapability::COLOR_TEMPERATURE)) {
ESP_LOGCONFIG(TAG,
" Min Mireds: %.1f\n"
" Max Mireds: %.1f",
traits.get_min_mireds(), traits.get_max_mireds());
" Min Mireds: %d\n"
" Max Mireds: %d",
(int) traits.get_min_mireds(), (int) traits.get_max_mireds());
}
}
void LightState::loop() {

View File

@@ -91,7 +91,7 @@ void MediaPlayerCall::perform() {
ESP_LOGD(TAG, " Media URL: %s", this->media_url_.value().c_str());
}
if (this->volume_.has_value()) {
ESP_LOGD(TAG, " Volume: %.2f", this->volume_.value());
ESP_LOGD(TAG, " Volume: %d%%", (int) (this->volume_.value() * 100.0f));
}
if (this->announcement_.has_value()) {
ESP_LOGD(TAG, " Announcement: %s", this->announcement_.value() ? "yes" : "no");

View File

@@ -22,7 +22,7 @@ void log_number(const char *tag, const char *prefix, const char *type, Number *o
void Number::publish_state(float state) {
this->set_has_state(true);
this->state = state;
ESP_LOGD(TAG, "'%s' >> %.2f", this->get_name().c_str(), state);
ESP_LOGD(TAG, "'%s' >> %s%d.%02d", this->get_name().c_str(), DECIMAL_2(state));
this->state_callback_.call(state);
#if defined(USE_NUMBER) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_number_update(this);

View File

@@ -13,8 +13,8 @@ void NumberCall::log_perform_warning_(const LogString *message) {
void NumberCall::log_perform_warning_value_range_(const LogString *comparison, const LogString *limit_type, float val,
float limit) {
ESP_LOGW(TAG, "'%s': %f %s %s %f", this->parent_->get_name().c_str(), val, LOG_STR_ARG(comparison),
LOG_STR_ARG(limit_type), limit);
ESP_LOGW(TAG, "'%s': %s%d.%02d %s %s %s%d.%02d", this->parent_->get_name().c_str(), DECIMAL_2(val),
LOG_STR_ARG(comparison), LOG_STR_ARG(limit_type), DECIMAL_2(limit));
}
NumberCall &NumberCall::set_value(float value) { return this->with_operation(NUMBER_OP_SET).with_value(value); }
@@ -120,7 +120,7 @@ void NumberCall::perform() {
return;
}
ESP_LOGD(TAG, " New value: %f", target_value);
ESP_LOGD(TAG, " New value: %s%d.%02d", DECIMAL_2(target_value));
this->parent_->control(target_value);
}

View File

@@ -1,6 +1,7 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "binary_output.h"
namespace esphome {
@@ -9,10 +10,10 @@ namespace output {
#define LOG_FLOAT_OUTPUT(this) \
LOG_BINARY_OUTPUT(this) \
if (this->max_power_ != 1.0f) { \
ESP_LOGCONFIG(TAG, " Max Power: %.1f%%", this->max_power_ * 100.0f); \
ESP_LOGCONFIG(TAG, " Max Power: %s%d.%d%%", DECIMAL_1(this->max_power_ * 100.0f)); \
} \
if (this->min_power_ != 0.0f) { \
ESP_LOGCONFIG(TAG, " Min Power: %.1f%%", this->min_power_ * 100.0f); \
ESP_LOGCONFIG(TAG, " Min Power: %s%d.%d%%", DECIMAL_1(this->min_power_ * 100.0f)); \
}
/** Base class for all output components that can output a variable level, like PWM.

View File

@@ -66,7 +66,7 @@ void Sensor::publish_state(float state) {
this->raw_state = state;
this->raw_callback_.call(state);
ESP_LOGV(TAG, "'%s': Received new state %f", this->name_.c_str(), state);
ESP_LOGV(TAG, "'%s': Received new state %s%d.%02d", this->name_.c_str(), DECIMAL_2(state));
if (this->filter_list_ == nullptr) {
this->internal_send_state_to_frontend(state);
@@ -115,8 +115,20 @@ float Sensor::get_raw_state() const { return this->raw_state; }
void Sensor::internal_send_state_to_frontend(float state) {
this->set_has_state(true);
this->state = state;
ESP_LOGD(TAG, "'%s' >> %.*f %s", this->get_name().c_str(), std::max(0, (int) this->get_accuracy_decimals()), state,
this->get_unit_of_measurement_ref().c_str());
// Use integer formatting to avoid pulling in _dtoa_r (~3.4KB)
// Format based on accuracy_decimals: 0 = integer, 1 = 1 decimal, 2+ = 2 decimals
int decimals = std::max(0, (int) this->get_accuracy_decimals());
if (decimals == 0) {
ESP_LOGD(TAG, "'%s' >> %d %s", this->get_name().c_str(), (int) state, this->get_unit_of_measurement_ref().c_str());
} else if (decimals == 1) {
int scaled = static_cast<int>(state * 10.0f);
ESP_LOGD(TAG, "'%s' >> %s%d.%d %s", this->get_name().c_str(), scaled < 0 ? "-" : "", std::abs(scaled / 10),
std::abs(scaled % 10), this->get_unit_of_measurement_ref().c_str());
} else {
int scaled = static_cast<int>(state * 100.0f);
ESP_LOGD(TAG, "'%s' >> %s%d.%02d %s", this->get_name().c_str(), scaled < 0 ? "-" : "", std::abs(scaled / 100),
std::abs(scaled % 100), this->get_unit_of_measurement_ref().c_str());
}
this->callback_.call(state);
#if defined(USE_SENSOR) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_sensor_update(this);

View File

@@ -30,7 +30,7 @@ void UpdateEntity::publish_state() {
}
if (this->update_info_.has_progress) {
ESP_LOGD(TAG, " Progress: %.0f%%", this->update_info_.progress);
ESP_LOGD(TAG, " Progress: %d%%", (int) this->update_info_.progress);
}
this->set_has_state(true);

View File

@@ -76,7 +76,7 @@ void ValveCall::perform() {
}
if (this->position_.has_value()) {
if (traits.get_supports_position()) {
ESP_LOGD(TAG, " Position: %.0f%%", *this->position_ * 100.0f);
ESP_LOGD(TAG, " Position: %d%%", (int) (*this->position_ * 100.0f));
} else {
ESP_LOGD(TAG, " Command: %s", LOG_STR_ARG(valve_command_to_str(*this->position_)));
}
@@ -96,7 +96,8 @@ void ValveCall::validate_() {
ESP_LOGW(TAG, "'%s' - This valve device does not support setting position!", this->parent_->get_name().c_str());
this->position_.reset();
} else if (pos < 0.0f || pos > 1.0f) {
ESP_LOGW(TAG, "'%s' - Position %.2f is out of range [0.0 - 1.0]", this->parent_->get_name().c_str(), pos);
ESP_LOGW(TAG, "'%s' - Position %s%d.%02d is out of range [0.0 - 1.0]", this->parent_->get_name().c_str(),
DECIMAL_2(pos));
this->position_ = clamp(pos, 0.0f, 1.0f);
}
}
@@ -132,7 +133,7 @@ void Valve::publish_state(bool save) {
ESP_LOGD(TAG, "'%s' >>", this->name_.c_str());
auto traits = this->get_traits();
if (traits.get_supports_position()) {
ESP_LOGD(TAG, " Position: %.0f%%", this->position * 100.0f);
ESP_LOGD(TAG, " Position: %d%%", (int) (this->position * 100.0f));
} else {
if (this->position == VALVE_OPEN) {
ESP_LOGD(TAG, " State: OPEN");

View File

@@ -84,7 +84,7 @@ void OTARequestHandler::report_ota_progress_(AsyncWebServerRequest *request) {
// access to the actual firmware size until the upload is complete. This is intentional
// as it still gives the user a reasonable progress indication.
percentage = (this->ota_read_length_ * 100.0f) / request->contentLength();
ESP_LOGD(TAG, "OTA in progress: %0.1f%%", percentage);
ESP_LOGD(TAG, "OTA in progress: %s%d.%d%%", DECIMAL_1(percentage));
} else {
ESP_LOGD(TAG, "OTA in progress: %" PRIu32 " bytes read", this->ota_read_length_);
}

View File

@@ -450,9 +450,12 @@ void log_update_interval(const char *tag, PollingComponent *component) {
if (update_interval == SCHEDULER_DONT_RUN) {
ESP_LOGCONFIG(tag, " Update Interval: never");
} else if (update_interval < 100) {
ESP_LOGCONFIG(tag, " Update Interval: %.3fs", update_interval / 1000.0f);
// Use integer math to avoid pulling in _dtoa_r (~3.4KB)
// update_interval is in ms, display as X.YYYs
ESP_LOGCONFIG(tag, " Update Interval: %d.%03ds", update_interval / 1000, update_interval % 1000);
} else {
ESP_LOGCONFIG(tag, " Update Interval: %.1fs", update_interval / 1000.0f);
// Display as X.Ys (1 decimal place)
ESP_LOGCONFIG(tag, " Update Interval: %d.%ds", update_interval / 1000, (update_interval % 1000) / 100);
}
}
float Component::get_actual_setup_priority() const {

View File

@@ -478,6 +478,44 @@ static inline void normalize_accuracy_decimals(float &value, int8_t &accuracy_de
}
}
// Power of 10 lookup table for accuracy_decimals 0-9
static const uint32_t POWERS_OF_10[] = {1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000};
// Format float to buffer without using %f (avoids _dtoa_r ~3.4KB)
static size_t format_float_to_buf(char *buf, size_t buf_size, float value, int8_t accuracy_decimals) {
if (buf_size == 0)
return 0;
// Handle sign
const char *sign = "";
if (value < 0) {
sign = "-";
value = -value;
}
// Clamp accuracy_decimals to supported range
if (accuracy_decimals > 9)
accuracy_decimals = 9;
int len;
if (accuracy_decimals == 0) {
// Integer only
len = snprintf(buf, buf_size, "%s%d", sign, static_cast<int>(value + 0.5f));
} else {
// Scale and round
uint32_t divisor = POWERS_OF_10[accuracy_decimals];
uint64_t scaled = static_cast<uint64_t>(value * divisor + 0.5f);
uint32_t integer_part = scaled / divisor;
uint32_t decimal_part = scaled % divisor;
// %0*d pads with leading zeros to match accuracy_decimals
len = snprintf(buf, buf_size, "%s%u.%0*u", sign, integer_part, accuracy_decimals, decimal_part);
}
if (len < 0)
return 0;
return static_cast<size_t>(len) >= buf_size ? buf_size - 1 : static_cast<size_t>(len);
}
std::string value_accuracy_to_string(float value, int8_t accuracy_decimals) {
char buf[VALUE_ACCURACY_MAX_LEN];
value_accuracy_to_buf(buf, value, accuracy_decimals);
@@ -486,12 +524,7 @@ std::string value_accuracy_to_string(float value, int8_t accuracy_decimals) {
size_t value_accuracy_to_buf(std::span<char, VALUE_ACCURACY_MAX_LEN> buf, float value, int8_t accuracy_decimals) {
normalize_accuracy_decimals(value, accuracy_decimals);
// snprintf returns chars that would be written (excluding null), or negative on error
int len = snprintf(buf.data(), buf.size(), "%.*f", accuracy_decimals, value);
if (len < 0)
return 0; // encoding error
// On truncation, snprintf returns would-be length; actual written is buf.size() - 1
return static_cast<size_t>(len) >= buf.size() ? buf.size() - 1 : static_cast<size_t>(len);
return format_float_to_buf(buf.data(), buf.size(), value, accuracy_decimals);
}
size_t value_accuracy_with_uom_to_buf(std::span<char, VALUE_ACCURACY_MAX_LEN> buf, float value,
@@ -500,25 +533,33 @@ size_t value_accuracy_with_uom_to_buf(std::span<char, VALUE_ACCURACY_MAX_LEN> bu
return value_accuracy_to_buf(buf, value, accuracy_decimals);
}
normalize_accuracy_decimals(value, accuracy_decimals);
// snprintf returns chars that would be written (excluding null), or negative on error
int len = snprintf(buf.data(), buf.size(), "%.*f %s", accuracy_decimals, value, unit_of_measurement.c_str());
if (len < 0)
return 0; // encoding error
// On truncation, snprintf returns would-be length; actual written is buf.size() - 1
return static_cast<size_t>(len) >= buf.size() ? buf.size() - 1 : static_cast<size_t>(len);
// Format value first, then append unit
size_t len = format_float_to_buf(buf.data(), buf.size(), value, accuracy_decimals);
if (len + 1 < buf.size()) {
// Append space and unit
int uom_len = snprintf(buf.data() + len, buf.size() - len, " %s", unit_of_measurement.c_str());
if (uom_len > 0) {
len += static_cast<size_t>(uom_len);
if (len >= buf.size())
len = buf.size() - 1;
}
}
return len;
}
int8_t step_to_accuracy_decimals(float step) {
// use printf %g to find number of digits based on temperature step
char buf[32];
snprintf(buf, sizeof buf, "%.5g", step);
std::string str{buf};
size_t dot_pos = str.find('.');
if (dot_pos == std::string::npos)
// Determine decimal places needed without using %g (avoids _dtoa_r)
if (step >= 1.0f)
return 0;
return str.length() - dot_pos - 1;
// Multiply by powers of 10 until we get an integer
for (int8_t decimals = 1; decimals <= 5; decimals++) {
float scaled = step * POWERS_OF_10[decimals];
// Check if scaled is close to an integer (within floating point tolerance)
if (std::abs(scaled - std::round(scaled)) < 0.001f)
return decimals;
}
return 5; // Max 5 decimal places
}
// Store BASE64 characters as array - automatically placed in flash/ROM on embedded platforms

View File

@@ -1917,4 +1917,45 @@ class CompactString {
static_assert(sizeof(CompactString) == 20, "CompactString must be exactly 20 bytes");
/// @name Float formatting helpers (avoid _dtoa_r bloat)
/// These helpers format floats as fixed-point integers for logging without pulling in
/// the ~3.4KB _dtoa_r library. They handle negative values correctly including -0.x cases.
/// Usage: ESP_LOGD(TAG, "Temp: %s%d.%d°C", DECIMAL_1(temp));
/// Usage: ESP_LOGD(TAG, "Value: %s%d.%02d", DECIMAL_2(value));
///@{
/// Helper for formatting floats with 1 decimal place
struct Decimal1 {
const char *sign;
int integer;
int decimal;
explicit Decimal1(float value) {
int scaled = static_cast<int>(value * 10.0f);
sign = scaled < 0 ? "-" : "";
integer = std::abs(scaled / 10);
decimal = std::abs(scaled % 10);
}
};
/// Helper for formatting floats with 2 decimal places
struct Decimal2 {
const char *sign;
int integer;
int decimal;
explicit Decimal2(float value) {
int scaled = static_cast<int>(value * 100.0f);
sign = scaled < 0 ? "-" : "";
integer = std::abs(scaled / 100);
decimal = std::abs(scaled % 100);
}
};
/// Format float with 1 decimal place - expands to 3 args for %s%d.%d
#define DECIMAL_1(v) esphome::Decimal1(v).sign, esphome::Decimal1(v).integer, esphome::Decimal1(v).decimal
/// Format float with 2 decimal places - expands to 3 args for %s%d.%02d
#define DECIMAL_2(v) esphome::Decimal2(v).sign, esphome::Decimal2(v).integer, esphome::Decimal2(v).decimal
///@}
} // namespace esphome

View File

@@ -296,14 +296,14 @@ void HOT Scheduler::set_retry_common_(Component *component, NameType name_type,
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
{
SchedulerNameLog name_log;
ESP_LOGVV(TAG, "set_retry(name='%s', initial_wait_time=%" PRIu32 ", max_attempts=%u, backoff_factor=%0.1f)",
ESP_LOGVV(TAG, "set_retry(name='%s', initial_wait_time=%" PRIu32 ", max_attempts=%u, backoff_factor=%s%d.%d)",
name_log.format(name_type, static_name, hash_or_id), initial_wait_time, max_attempts,
backoff_increase_factor);
DECIMAL_1(backoff_increase_factor));
}
#endif
if (backoff_increase_factor < 0.0001) {
ESP_LOGE(TAG, "set_retry: backoff_factor %0.1f too small, using 1.0: %s", backoff_increase_factor,
ESP_LOGE(TAG, "set_retry: backoff_factor %s%d.%d too small, using 1.0: %s", DECIMAL_1(backoff_increase_factor),
(name_type == NameType::STATIC_STRING && static_name) ? static_name : "");
backoff_increase_factor = 1;
}