[web_server] Use stack buffers for value formatting to reduce flash usage (#12575)

This commit is contained in:
J. Nick Koston
2025-12-22 11:56:07 -10:00
committed by GitHub
parent f238f93312
commit af0d4d2c2c
3 changed files with 68 additions and 45 deletions

View File

@@ -432,7 +432,7 @@ static void set_json_value(JsonObject &root, EntityBase *obj, const char *prefix
} }
template<typename T> template<typename T>
static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const char *prefix, const std::string &state, static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const char *prefix, const char *state,
const T &value, JsonDetail start_config) { const T &value, JsonDetail start_config) {
set_json_value(root, obj, prefix, value, start_config); set_json_value(root, obj, prefix, value, start_config);
root[ESPHOME_F("state")] = state; root[ESPHOME_F("state")] = state;
@@ -475,9 +475,10 @@ std::string WebServer::sensor_json_(sensor::Sensor *obj, float value, JsonDetail
JsonObject root = builder.root(); JsonObject root = builder.root();
const auto uom_ref = obj->get_unit_of_measurement_ref(); const auto uom_ref = obj->get_unit_of_measurement_ref();
char buf[VALUE_ACCURACY_MAX_LEN];
std::string state = const char *state = std::isnan(value)
std::isnan(value) ? "NA" : value_accuracy_with_uom_to_string(value, obj->get_accuracy_decimals(), uom_ref); ? "NA"
: (value_accuracy_with_uom_to_buf(buf, value, obj->get_accuracy_decimals(), uom_ref), buf);
set_json_icon_state_value(root, obj, "sensor", state, value, start_config); set_json_icon_state_value(root, obj, "sensor", state, value, start_config);
if (start_config == DETAIL_ALL) { if (start_config == DETAIL_ALL) {
this->add_sorting_info_(root, obj); this->add_sorting_info_(root, obj);
@@ -522,7 +523,7 @@ std::string WebServer::text_sensor_json_(text_sensor::TextSensor *obj, const std
json::JsonBuilder builder; json::JsonBuilder builder;
JsonObject root = builder.root(); JsonObject root = builder.root();
set_json_icon_state_value(root, obj, "text_sensor", value, value, start_config); set_json_icon_state_value(root, obj, "text_sensor", value.c_str(), value.c_str(), start_config);
if (start_config == DETAIL_ALL) { if (start_config == DETAIL_ALL) {
this->add_sorting_info_(root, obj); this->add_sorting_info_(root, obj);
} }
@@ -974,21 +975,20 @@ std::string WebServer::number_json_(number::Number *obj, float value, JsonDetail
JsonObject root = builder.root(); JsonObject root = builder.root();
const auto uom_ref = obj->traits.get_unit_of_measurement_ref(); const auto uom_ref = obj->traits.get_unit_of_measurement_ref();
const int8_t accuracy = step_to_accuracy_decimals(obj->traits.get_step());
std::string val_str = std::isnan(value) // Need two buffers: one for value, one for state with UOM
? "\"NaN\"" char val_buf[VALUE_ACCURACY_MAX_LEN];
: value_accuracy_to_string(value, step_to_accuracy_decimals(obj->traits.get_step())); char state_buf[VALUE_ACCURACY_MAX_LEN];
std::string state_str = std::isnan(value) ? "NA" const char *val_str = std::isnan(value) ? "\"NaN\"" : (value_accuracy_to_buf(val_buf, value, accuracy), val_buf);
: value_accuracy_with_uom_to_string( const char *state_str =
value, step_to_accuracy_decimals(obj->traits.get_step()), uom_ref); std::isnan(value) ? "NA" : (value_accuracy_with_uom_to_buf(state_buf, value, accuracy, uom_ref), state_buf);
set_json_icon_state_value(root, obj, "number", state_str, val_str, start_config); set_json_icon_state_value(root, obj, "number", state_str, val_str, start_config);
if (start_config == DETAIL_ALL) { if (start_config == DETAIL_ALL) {
root[ESPHOME_F("min_value")] = // ArduinoJson copies the string immediately, so we can reuse val_buf
value_accuracy_to_string(obj->traits.get_min_value(), step_to_accuracy_decimals(obj->traits.get_step())); root[ESPHOME_F("min_value")] = (value_accuracy_to_buf(val_buf, obj->traits.get_min_value(), accuracy), val_buf);
root[ESPHOME_F("max_value")] = root[ESPHOME_F("max_value")] = (value_accuracy_to_buf(val_buf, obj->traits.get_max_value(), accuracy), val_buf);
value_accuracy_to_string(obj->traits.get_max_value(), step_to_accuracy_decimals(obj->traits.get_step())); root[ESPHOME_F("step")] = (value_accuracy_to_buf(val_buf, obj->traits.get_step(), accuracy), val_buf);
root[ESPHOME_F("step")] =
value_accuracy_to_string(obj->traits.get_step(), step_to_accuracy_decimals(obj->traits.get_step()));
root[ESPHOME_F("mode")] = (int) obj->traits.get_mode(); root[ESPHOME_F("mode")] = (int) obj->traits.get_mode();
if (!uom_ref.empty()) if (!uom_ref.empty())
root[ESPHOME_F("uom")] = uom_ref; root[ESPHOME_F("uom")] = uom_ref;
@@ -1230,8 +1230,8 @@ std::string WebServer::text_json_(text::Text *obj, const std::string &value, Jso
json::JsonBuilder builder; json::JsonBuilder builder;
JsonObject root = builder.root(); JsonObject root = builder.root();
std::string state = obj->traits.get_mode() == text::TextMode::TEXT_MODE_PASSWORD ? "********" : value; const char *state = obj->traits.get_mode() == text::TextMode::TEXT_MODE_PASSWORD ? "********" : value.c_str();
set_json_icon_state_value(root, obj, "text", state, value, start_config); set_json_icon_state_value(root, obj, "text", state, value.c_str(), start_config);
root[ESPHOME_F("min_length")] = obj->traits.get_min_length(); root[ESPHOME_F("min_length")] = obj->traits.get_min_length();
root[ESPHOME_F("max_length")] = obj->traits.get_max_length(); root[ESPHOME_F("max_length")] = obj->traits.get_max_length();
root[ESPHOME_F("pattern")] = obj->traits.get_pattern_c_str(); root[ESPHOME_F("pattern")] = obj->traits.get_pattern_c_str();
@@ -1359,6 +1359,7 @@ std::string WebServer::climate_json_(climate::Climate *obj, JsonDetail start_con
int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals(); int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals();
int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals(); int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals();
char buf[PSTR_LOCAL_SIZE]; char buf[PSTR_LOCAL_SIZE];
char temp_buf[VALUE_ACCURACY_MAX_LEN];
if (start_config == DETAIL_ALL) { if (start_config == DETAIL_ALL) {
JsonArray opt = root[ESPHOME_F("modes")].to<JsonArray>(); JsonArray opt = root[ESPHOME_F("modes")].to<JsonArray>();
@@ -1395,8 +1396,10 @@ std::string WebServer::climate_json_(climate::Climate *obj, JsonDetail start_con
bool has_state = false; bool has_state = false;
root[ESPHOME_F("mode")] = PSTR_LOCAL(climate_mode_to_string(obj->mode)); root[ESPHOME_F("mode")] = PSTR_LOCAL(climate_mode_to_string(obj->mode));
root[ESPHOME_F("max_temp")] = value_accuracy_to_string(traits.get_visual_max_temperature(), target_accuracy); root[ESPHOME_F("max_temp")] =
root[ESPHOME_F("min_temp")] = value_accuracy_to_string(traits.get_visual_min_temperature(), target_accuracy); (value_accuracy_to_buf(temp_buf, traits.get_visual_max_temperature(), target_accuracy), temp_buf);
root[ESPHOME_F("min_temp")] =
(value_accuracy_to_buf(temp_buf, traits.get_visual_min_temperature(), target_accuracy), temp_buf);
root[ESPHOME_F("step")] = traits.get_visual_target_temperature_step(); root[ESPHOME_F("step")] = traits.get_visual_target_temperature_step();
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) { if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) {
root[ESPHOME_F("action")] = PSTR_LOCAL(climate_action_to_string(obj->action)); root[ESPHOME_F("action")] = PSTR_LOCAL(climate_action_to_string(obj->action));
@@ -1419,23 +1422,26 @@ std::string WebServer::climate_json_(climate::Climate *obj, JsonDetail start_con
root[ESPHOME_F("swing_mode")] = PSTR_LOCAL(climate_swing_mode_to_string(obj->swing_mode)); root[ESPHOME_F("swing_mode")] = PSTR_LOCAL(climate_swing_mode_to_string(obj->swing_mode));
} }
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) { if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) {
if (!std::isnan(obj->current_temperature)) { root[ESPHOME_F("current_temperature")] =
root[ESPHOME_F("current_temperature")] = value_accuracy_to_string(obj->current_temperature, current_accuracy); std::isnan(obj->current_temperature)
} else { ? "NA"
root[ESPHOME_F("current_temperature")] = "NA"; : (value_accuracy_to_buf(temp_buf, obj->current_temperature, current_accuracy), temp_buf);
}
} }
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) { climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
root[ESPHOME_F("target_temperature_low")] = value_accuracy_to_string(obj->target_temperature_low, target_accuracy); root[ESPHOME_F("target_temperature_low")] =
(value_accuracy_to_buf(temp_buf, obj->target_temperature_low, target_accuracy), temp_buf);
root[ESPHOME_F("target_temperature_high")] = root[ESPHOME_F("target_temperature_high")] =
value_accuracy_to_string(obj->target_temperature_high, target_accuracy); (value_accuracy_to_buf(temp_buf, obj->target_temperature_high, target_accuracy), temp_buf);
if (!has_state) { if (!has_state) {
root[ESPHOME_F("state")] = value_accuracy_to_string( root[ESPHOME_F("state")] =
(obj->target_temperature_high + obj->target_temperature_low) / 2.0f, target_accuracy); (value_accuracy_to_buf(temp_buf, (obj->target_temperature_high + obj->target_temperature_low) / 2.0f,
target_accuracy),
temp_buf);
} }
} else { } else {
root[ESPHOME_F("target_temperature")] = value_accuracy_to_string(obj->target_temperature, target_accuracy); root[ESPHOME_F("target_temperature")] =
(value_accuracy_to_buf(temp_buf, obj->target_temperature, target_accuracy), temp_buf);
if (!has_state) if (!has_state)
root[ESPHOME_F("state")] = root[ESPHOME_F("target_temperature")]; root[ESPHOME_F("state")] = root[ESPHOME_F("target_temperature")];
} }

View File

@@ -383,23 +383,33 @@ static inline void normalize_accuracy_decimals(float &value, int8_t &accuracy_de
} }
std::string value_accuracy_to_string(float value, int8_t accuracy_decimals) { std::string value_accuracy_to_string(float value, int8_t accuracy_decimals) {
normalize_accuracy_decimals(value, accuracy_decimals); char buf[VALUE_ACCURACY_MAX_LEN];
char tmp[32]; // should be enough, but we should maybe improve this at some point. value_accuracy_to_buf(buf, value, accuracy_decimals);
snprintf(tmp, sizeof(tmp), "%.*f", accuracy_decimals, value); return std::string(buf);
return std::string(tmp);
} }
std::string value_accuracy_with_uom_to_string(float value, int8_t accuracy_decimals, StringRef unit_of_measurement) { 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); normalize_accuracy_decimals(value, accuracy_decimals);
// Buffer sized for float (up to ~15 chars) + space + typical UOM (usually <20 chars like "μS/cm") // snprintf returns chars that would be written (excluding null), or negative on error
// snprintf truncates safely if exceeded, though ESPHome UOMs are typically short int len = snprintf(buf.data(), buf.size(), "%.*f", accuracy_decimals, value);
char tmp[64]; 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);
}
size_t value_accuracy_with_uom_to_buf(std::span<char, VALUE_ACCURACY_MAX_LEN> buf, float value,
int8_t accuracy_decimals, StringRef unit_of_measurement) {
if (unit_of_measurement.empty()) { if (unit_of_measurement.empty()) {
snprintf(tmp, sizeof(tmp), "%.*f", accuracy_decimals, value); return value_accuracy_to_buf(buf, value, accuracy_decimals);
} else {
snprintf(tmp, sizeof(tmp), "%.*f %s", accuracy_decimals, value, unit_of_measurement.c_str());
} }
return std::string(tmp); 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);
} }
int8_t step_to_accuracy_decimals(float step) { int8_t step_to_accuracy_decimals(float step) {

View File

@@ -886,8 +886,15 @@ ParseOnOffState parse_on_off(const char *str, const char *on = nullptr, const ch
/// Create a string from a value and an accuracy in decimals. /// Create a string from a value and an accuracy in decimals.
std::string value_accuracy_to_string(float value, int8_t accuracy_decimals); std::string value_accuracy_to_string(float value, int8_t accuracy_decimals);
/// Create a string from a value, an accuracy in decimals, and a unit of measurement.
std::string value_accuracy_with_uom_to_string(float value, int8_t accuracy_decimals, StringRef unit_of_measurement); /// Maximum buffer size for value_accuracy formatting (float ~15 chars + space + UOM ~40 chars + null)
static constexpr size_t VALUE_ACCURACY_MAX_LEN = 64;
/// Format value with accuracy to buffer, returns chars written (excluding null)
size_t value_accuracy_to_buf(std::span<char, VALUE_ACCURACY_MAX_LEN> buf, float value, int8_t accuracy_decimals);
/// Format value with accuracy and UOM to buffer, returns chars written (excluding null)
size_t value_accuracy_with_uom_to_buf(std::span<char, VALUE_ACCURACY_MAX_LEN> buf, float value,
int8_t accuracy_decimals, StringRef unit_of_measurement);
/// Derive accuracy in decimals from an increment step. /// Derive accuracy in decimals from an increment step.
int8_t step_to_accuracy_decimals(float step); int8_t step_to_accuracy_decimals(float step);