diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 33141c2049..95e0d13b58 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -381,11 +381,14 @@ void WebServer::handle_js_request(AsyncWebServerRequest *request) { #endif // Helper functions to reduce code size by avoiding macro expansion -static void set_json_id(JsonObject &root, EntityBase *obj, const std::string &id, JsonDetail start_config) { - root["id"] = id; +static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, JsonDetail start_config) { + char id_buf[160]; // object_id can be up to 128 chars + prefix + dash + null + const auto &object_id = obj->get_object_id(); + snprintf(id_buf, sizeof(id_buf), "%s-%s", prefix, object_id.c_str()); + root["id"] = id_buf; if (start_config == DETAIL_ALL) { root["name"] = obj->get_name(); - root["icon"] = obj->get_icon(); + root["icon"] = obj->get_icon_ref(); root["entity_category"] = obj->get_entity_category(); bool is_disabled = obj->is_disabled_by_default(); if (is_disabled) @@ -393,17 +396,19 @@ static void set_json_id(JsonObject &root, EntityBase *obj, const std::string &id } } +// Keep as separate function even though only used once: reduces code size by ~48 bytes +// by allowing compiler to share code between template instantiations (bool, float, etc.) template -static void set_json_value(JsonObject &root, EntityBase *obj, const std::string &id, const T &value, +static void set_json_value(JsonObject &root, EntityBase *obj, const char *prefix, const T &value, JsonDetail start_config) { - set_json_id(root, obj, id, start_config); + set_json_id(root, obj, prefix, start_config); root["value"] = value; } template -static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const std::string &id, - const std::string &state, const T &value, JsonDetail start_config) { - set_json_value(root, obj, id, value, start_config); +static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const char *prefix, const std::string &state, + const T &value, JsonDetail start_config) { + set_json_value(root, obj, prefix, value, start_config); root["state"] = state; } @@ -442,20 +447,20 @@ std::string WebServer::sensor_json(sensor::Sensor *obj, float value, JsonDetail json::JsonBuilder builder; JsonObject root = builder.root(); + const auto uom_ref = obj->get_unit_of_measurement_ref(); + // Build JSON directly inline std::string state; if (std::isnan(value)) { state = "NA"; } else { - state = value_accuracy_to_string(value, obj->get_accuracy_decimals()); - if (!obj->get_unit_of_measurement().empty()) - state += " " + obj->get_unit_of_measurement(); + state = value_accuracy_with_uom_to_string(value, obj->get_accuracy_decimals(), uom_ref); } - set_json_icon_state_value(root, obj, "sensor-" + obj->get_object_id(), state, value, start_config); + set_json_icon_state_value(root, obj, "sensor", state, value, start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); - if (!obj->get_unit_of_measurement().empty()) - root["uom"] = obj->get_unit_of_measurement(); + if (!uom_ref.empty()) + root["uom"] = uom_ref; } return builder.serialize(); @@ -494,7 +499,7 @@ std::string WebServer::text_sensor_json(text_sensor::TextSensor *obj, const std: json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_icon_state_value(root, obj, "text_sensor-" + obj->get_object_id(), value, value, start_config); + set_json_icon_state_value(root, obj, "text_sensor", value, value, start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); } @@ -567,7 +572,7 @@ std::string WebServer::switch_json(switch_::Switch *obj, bool value, JsonDetail json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_icon_state_value(root, obj, "switch-" + obj->get_object_id(), value ? "ON" : "OFF", value, start_config); + set_json_icon_state_value(root, obj, "switch", value ? "ON" : "OFF", value, start_config); if (start_config == DETAIL_ALL) { root["assumed_state"] = obj->assumed_state(); this->add_sorting_info_(root, obj); @@ -607,7 +612,7 @@ std::string WebServer::button_json(button::Button *obj, JsonDetail start_config) json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_id(root, obj, "button-" + obj->get_object_id(), start_config); + set_json_id(root, obj, "button", start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); } @@ -647,8 +652,7 @@ std::string WebServer::binary_sensor_json(binary_sensor::BinarySensor *obj, bool json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_icon_state_value(root, obj, "binary_sensor-" + obj->get_object_id(), value ? "ON" : "OFF", value, - start_config); + set_json_icon_state_value(root, obj, "binary_sensor", value ? "ON" : "OFF", value, start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); } @@ -717,8 +721,7 @@ std::string WebServer::fan_json(fan::Fan *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_icon_state_value(root, obj, "fan-" + obj->get_object_id(), obj->state ? "ON" : "OFF", obj->state, - start_config); + set_json_icon_state_value(root, obj, "fan", obj->state ? "ON" : "OFF", obj->state, start_config); const auto traits = obj->get_traits(); if (traits.supports_speed()) { root["speed_level"] = obj->speed; @@ -793,7 +796,7 @@ std::string WebServer::light_json(light::LightState *obj, JsonDetail start_confi json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_id(root, obj, "light-" + obj->get_object_id(), start_config); + set_json_id(root, obj, "light", start_config); root["state"] = obj->remote_values.is_on() ? "ON" : "OFF"; light::LightJSONSchema::dump_json(*obj, root); @@ -881,8 +884,8 @@ std::string WebServer::cover_json(cover::Cover *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_icon_state_value(root, obj, "cover-" + obj->get_object_id(), obj->is_fully_closed() ? "CLOSED" : "OPEN", - obj->position, start_config); + set_json_icon_state_value(root, obj, "cover", obj->is_fully_closed() ? "CLOSED" : "OPEN", obj->position, + start_config); root["current_operation"] = cover::cover_operation_to_str(obj->current_operation); if (obj->get_traits().get_supports_position()) @@ -939,7 +942,9 @@ std::string WebServer::number_json(number::Number *obj, float value, JsonDetail json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_id(root, obj, "number-" + obj->get_object_id(), start_config); + const auto uom_ref = obj->traits.get_unit_of_measurement_ref(); + + set_json_id(root, obj, "number", start_config); if (start_config == DETAIL_ALL) { root["min_value"] = value_accuracy_to_string(obj->traits.get_min_value(), step_to_accuracy_decimals(obj->traits.get_step())); @@ -947,8 +952,8 @@ std::string WebServer::number_json(number::Number *obj, float value, JsonDetail value_accuracy_to_string(obj->traits.get_max_value(), step_to_accuracy_decimals(obj->traits.get_step())); root["step"] = value_accuracy_to_string(obj->traits.get_step(), step_to_accuracy_decimals(obj->traits.get_step())); root["mode"] = (int) obj->traits.get_mode(); - if (!obj->traits.get_unit_of_measurement().empty()) - root["uom"] = obj->traits.get_unit_of_measurement(); + if (!uom_ref.empty()) + root["uom"] = uom_ref; this->add_sorting_info_(root, obj); } if (std::isnan(value)) { @@ -956,10 +961,8 @@ std::string WebServer::number_json(number::Number *obj, float value, JsonDetail root["state"] = "NA"; } else { root["value"] = value_accuracy_to_string(value, step_to_accuracy_decimals(obj->traits.get_step())); - std::string state = value_accuracy_to_string(value, step_to_accuracy_decimals(obj->traits.get_step())); - if (!obj->traits.get_unit_of_measurement().empty()) - state += " " + obj->traits.get_unit_of_measurement(); - root["state"] = state; + root["state"] = + value_accuracy_with_uom_to_string(value, step_to_accuracy_decimals(obj->traits.get_step()), uom_ref); } return builder.serialize(); @@ -1013,7 +1016,7 @@ std::string WebServer::date_json(datetime::DateEntity *obj, JsonDetail start_con json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_id(root, obj, "date-" + obj->get_object_id(), start_config); + set_json_id(root, obj, "date", start_config); std::string value = str_sprintf("%d-%02d-%02d", obj->year, obj->month, obj->day); root["value"] = value; root["state"] = value; @@ -1071,7 +1074,7 @@ std::string WebServer::time_json(datetime::TimeEntity *obj, JsonDetail start_con json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_id(root, obj, "time-" + obj->get_object_id(), start_config); + set_json_id(root, obj, "time", start_config); std::string value = str_sprintf("%02d:%02d:%02d", obj->hour, obj->minute, obj->second); root["value"] = value; root["state"] = value; @@ -1129,7 +1132,7 @@ std::string WebServer::datetime_json(datetime::DateTimeEntity *obj, JsonDetail s json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_id(root, obj, "datetime-" + obj->get_object_id(), start_config); + set_json_id(root, obj, "datetime", start_config); std::string value = str_sprintf("%d-%02d-%02d %02d:%02d:%02d", obj->year, obj->month, obj->day, obj->hour, obj->minute, obj->second); root["value"] = value; @@ -1184,7 +1187,7 @@ std::string WebServer::text_json(text::Text *obj, const std::string &value, Json json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_id(root, obj, "text-" + obj->get_object_id(), start_config); + set_json_id(root, obj, "text", start_config); root["min_length"] = obj->traits.get_min_length(); root["max_length"] = obj->traits.get_max_length(); root["pattern"] = obj->traits.get_pattern(); @@ -1245,7 +1248,7 @@ std::string WebServer::select_json(select::Select *obj, const std::string &value json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_icon_state_value(root, obj, "select-" + obj->get_object_id(), value, value, start_config); + set_json_icon_state_value(root, obj, "select", value, value, start_config); if (start_config == DETAIL_ALL) { JsonArray opt = root["option"].to(); for (auto &option : obj->traits.get_options()) { @@ -1314,7 +1317,7 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_id(root, obj, "climate-" + obj->get_object_id(), start_config); + set_json_id(root, obj, "climate", start_config); const auto traits = obj->get_traits(); int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals(); int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals(); @@ -1467,8 +1470,7 @@ std::string WebServer::lock_json(lock::Lock *obj, lock::LockState value, JsonDet json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_icon_state_value(root, obj, "lock-" + obj->get_object_id(), lock::lock_state_to_string(value), value, - start_config); + set_json_icon_state_value(root, obj, "lock", lock::lock_state_to_string(value), value, start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); } @@ -1546,8 +1548,8 @@ std::string WebServer::valve_json(valve::Valve *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_icon_state_value(root, obj, "valve-" + obj->get_object_id(), obj->is_fully_closed() ? "CLOSED" : "OPEN", - obj->position, start_config); + set_json_icon_state_value(root, obj, "valve", obj->is_fully_closed() ? "CLOSED" : "OPEN", obj->position, + start_config); root["current_operation"] = valve::valve_operation_to_str(obj->current_operation); if (obj->get_traits().get_supports_position()) @@ -1630,8 +1632,8 @@ std::string WebServer::alarm_control_panel_json(alarm_control_panel::AlarmContro JsonObject root = builder.root(); char buf[16]; - set_json_icon_state_value(root, obj, "alarm-control-panel-" + obj->get_object_id(), - PSTR_LOCAL(alarm_control_panel_state_to_string(value)), value, start_config); + set_json_icon_state_value(root, obj, "alarm-control-panel", PSTR_LOCAL(alarm_control_panel_state_to_string(value)), + value, start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); } @@ -1676,7 +1678,7 @@ std::string WebServer::event_json(event::Event *obj, const std::string &event_ty json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_id(root, obj, "event-" + obj->get_object_id(), start_config); + set_json_id(root, obj, "event", start_config); if (!event_type.empty()) { root["event_type"] = event_type; } @@ -1685,7 +1687,7 @@ std::string WebServer::event_json(event::Event *obj, const std::string &event_ty for (auto const &event_type : obj->get_event_types()) { event_types.add(event_type); } - root["device_class"] = obj->get_device_class(); + root["device_class"] = obj->get_device_class_ref(); this->add_sorting_info_(root, obj); } @@ -1748,7 +1750,7 @@ std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_c json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_id(root, obj, "update-" + obj->get_object_id(), start_config); + set_json_id(root, obj, "update", start_config); root["value"] = obj->update_info.latest_version; root["state"] = update_state_to_string(obj->state); if (start_config == DETAIL_ALL) { diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 662d0d29e9..c79682fb41 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -3,6 +3,7 @@ #include "esphome/core/defines.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" +#include "esphome/core/string_ref.h" #include #include @@ -348,17 +349,32 @@ ParseOnOffState parse_on_off(const char *str, const char *on, const char *off) { return PARSE_NONE; } -std::string value_accuracy_to_string(float value, int8_t accuracy_decimals) { +static inline void normalize_accuracy_decimals(float &value, int8_t &accuracy_decimals) { if (accuracy_decimals < 0) { auto multiplier = powf(10.0f, accuracy_decimals); value = roundf(value * multiplier) / multiplier; accuracy_decimals = 0; } +} + +std::string value_accuracy_to_string(float value, int8_t accuracy_decimals) { + normalize_accuracy_decimals(value, accuracy_decimals); char tmp[32]; // should be enough, but we should maybe improve this at some point. snprintf(tmp, sizeof(tmp), "%.*f", accuracy_decimals, value); return std::string(tmp); } +std::string value_accuracy_with_uom_to_string(float value, int8_t accuracy_decimals, StringRef unit_of_measurement) { + normalize_accuracy_decimals(value, accuracy_decimals); + char tmp[64]; // Increased to accommodate unit of measurement + if (unit_of_measurement.empty()) { + snprintf(tmp, sizeof(tmp), "%.*f", accuracy_decimals, value); + } else { + snprintf(tmp, sizeof(tmp), "%.*f %s", accuracy_decimals, value, unit_of_measurement.c_str()); + } + return std::string(tmp); +} + int8_t step_to_accuracy_decimals(float step) { // use printf %g to find number of digits based on temperature step char buf[32]; diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 39d39c1c94..51115105ac 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -14,6 +14,10 @@ #include "esphome/core/optional.h" +namespace esphome { +class StringRef; +} + #ifdef USE_ESP8266 #include #endif @@ -603,6 +607,8 @@ ParseOnOffState parse_on_off(const char *str, const char *on = nullptr, const ch /// Create a string from a value and an accuracy in 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); /// Derive accuracy in decimals from an increment step. int8_t step_to_accuracy_decimals(float step);