From ddd43f466a75cce0f990ba0ddea4020a2eefe713 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 22 Dec 2025 17:22:14 -1000 Subject: [PATCH] refactor to reduce complexity --- esphome/components/web_server/web_server.cpp | 153 +++++++++++++------ esphome/components/web_server/web_server.h | 75 +-------- 2 files changed, 114 insertions(+), 114 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 170b621d89..8213c035ba 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -62,7 +62,6 @@ static UrlMatch match_url(const char *url_ptr, size_t url_len, bool only_domain) #ifdef USE_DEVICES match.device_name = nullptr; match.device_name_len = 0; - match.matched_as_two_segment_subdevice = false; #endif // URL must start with '/' @@ -163,12 +162,52 @@ static UrlMatch match_url(const char *url_ptr, size_t url_len, bool only_domain) return match; } -bool UrlMatch::id_equals_entity(EntityBase *entity) const { - bool used_deprecated_format = false; - bool matches = this->matches_entity(entity, used_deprecated_format); +EntityMatchResult UrlMatch::match_entity(EntityBase *entity) const { + EntityMatchResult result{false, this->method_len == 0}; - if (matches && used_deprecated_format) { - // Log deprecation warning when old object_id URL format is used +#ifdef USE_DEVICES + Device *entity_device = entity->get_device(); + bool url_has_device = (this->device_name_len > 0); + bool entity_has_device = (entity_device != nullptr); + + if (url_has_device) { + // URL has explicit device segment (3+ segments) - must match device + if (!entity_has_device) + return result; + const char *entity_device_name = entity_device->get_name(); + if (this->device_name_len != strlen(entity_device_name) || + memcmp(this->device_name, entity_device_name, this->device_name_len) != 0) + return result; + } else if (entity_has_device) { + // Entity has device but URL has only 2 segments (id/method) + // Try interpreting as device/entity: id=device_name, method=entity_name + if (this->method_len == 0) + return result; // Need 2 segments for this interpretation + const char *entity_device_name = entity_device->get_name(); + if (this->id_len == strlen(entity_device_name) && memcmp(this->id, entity_device_name, this->id_len) == 0) { + const StringRef &name_ref = entity->get_name(); + if (this->method_len == name_ref.size() && memcmp(this->method, name_ref.c_str(), this->method_len) == 0) { + // Matched: id=device, method=entity_name, so method is effectively empty + return {true, true}; + } + } + return result; // No match + } +#endif + + // Try matching by entity name (new format) + const StringRef &name_ref = entity->get_name(); + if (this->id_matches(name_ref.c_str(), name_ref.size())) { + result.matched = true; + return result; + } + + // Fall back to object_id (deprecated format) + char object_id_buf[OBJECT_ID_MAX_LEN]; + StringRef object_id = entity->get_object_id_to(object_id_buf); + if (this->id_matches(object_id.c_str(), object_id.size())) { + result.matched = true; + // Log deprecation warning #ifdef USE_DEVICES Device *device = entity->get_device(); if (device != nullptr) { @@ -188,7 +227,7 @@ bool UrlMatch::id_equals_entity(EntityBase *entity) const { } } - return matches; + return result; } #if !defined(USE_ESP32) && defined(USE_ARDUINO) @@ -572,10 +611,11 @@ void WebServer::on_sensor_update(sensor::Sensor *obj) { } void WebServer::handle_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (sensor::Sensor *obj : App.get_sensors()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; // Note: request->method() is always HTTP_GET here (canHandle ensures this) - if (match.method_empty()) { + if (entity_match.action_is_empty) { auto detail = get_request_detail(request); std::string data = this->sensor_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); @@ -618,10 +658,11 @@ void WebServer::on_text_sensor_update(text_sensor::TextSensor *obj) { } void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (text_sensor::TextSensor *obj : App.get_text_sensors()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; // Note: request->method() is always HTTP_GET here (canHandle ensures this) - if (match.method_empty()) { + if (entity_match.action_is_empty) { auto detail = get_request_detail(request); std::string data = this->text_sensor_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); @@ -660,10 +701,11 @@ void WebServer::on_switch_update(switch_::Switch *obj) { } void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (switch_::Switch *obj : App.get_switches()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); std::string data = this->switch_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); @@ -729,9 +771,10 @@ std::string WebServer::switch_json_(switch_::Switch *obj, bool value, JsonDetail #ifdef USE_BUTTON void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (button::Button *obj : App.get_buttons()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); std::string data = this->button_json_(obj, detail); request->send(200, "application/json", data.c_str()); @@ -773,10 +816,11 @@ void WebServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj) { } void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (binary_sensor::BinarySensor *obj : App.get_binary_sensors()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; // Note: request->method() is always HTTP_GET here (canHandle ensures this) - if (match.method_empty()) { + if (entity_match.action_is_empty) { auto detail = get_request_detail(request); std::string data = this->binary_sensor_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); @@ -814,10 +858,11 @@ void WebServer::on_fan_update(fan::Fan *obj) { } void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (fan::Fan *obj : App.get_fans()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); std::string data = this->fan_json_(obj, detail); request->send(200, "application/json", data.c_str()); @@ -894,10 +939,11 @@ void WebServer::on_light_update(light::LightState *obj) { } void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (light::LightState *obj : App.get_lights()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); std::string data = this->light_json_(obj, detail); request->send(200, "application/json", data.c_str()); @@ -972,10 +1018,11 @@ void WebServer::on_cover_update(cover::Cover *obj) { } void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (cover::Cover *obj : App.get_covers()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); std::string data = this->cover_json_(obj, detail); request->send(200, "application/json", data.c_str()); @@ -1060,10 +1107,11 @@ void WebServer::on_number_update(number::Number *obj) { } void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_numbers()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); std::string data = this->number_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); @@ -1127,9 +1175,10 @@ void WebServer::on_date_update(datetime::DateEntity *obj) { } void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_dates()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); std::string data = this->date_json_(obj, detail); request->send(200, "application/json", data.c_str()); @@ -1190,9 +1239,10 @@ void WebServer::on_time_update(datetime::TimeEntity *obj) { } void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_times()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); std::string data = this->time_json_(obj, detail); request->send(200, "application/json", data.c_str()); @@ -1252,9 +1302,10 @@ void WebServer::on_datetime_update(datetime::DateTimeEntity *obj) { } void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_datetimes()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); std::string data = this->datetime_json_(obj, detail); request->send(200, "application/json", data.c_str()); @@ -1316,10 +1367,11 @@ void WebServer::on_text_update(text::Text *obj) { } void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_texts()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); std::string data = this->text_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); @@ -1372,10 +1424,11 @@ void WebServer::on_select_update(select::Select *obj) { } void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_selects()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); std::string data = this->select_json_(obj, obj->has_state() ? obj->current_option() : "", detail); request->send(200, "application/json", data.c_str()); @@ -1429,10 +1482,11 @@ void WebServer::on_climate_update(climate::Climate *obj) { } void WebServer::handle_climate_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_climates()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); std::string data = this->climate_json_(obj, detail); request->send(200, "application/json", data.c_str()); @@ -1579,10 +1633,11 @@ void WebServer::on_lock_update(lock::Lock *obj) { } void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (lock::Lock *obj : App.get_locks()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); std::string data = this->lock_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); @@ -1653,10 +1708,11 @@ void WebServer::on_valve_update(valve::Valve *obj) { } void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (valve::Valve *obj : App.get_valves()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); std::string data = this->valve_json_(obj, detail); request->send(200, "application/json", data.c_str()); @@ -1737,10 +1793,11 @@ void WebServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlP } void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (alarm_control_panel::AlarmControlPanel *obj : App.get_alarm_control_panels()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); std::string data = this->alarm_control_panel_json_(obj, obj->get_state(), detail); request->send(200, "application/json", data.c_str()); @@ -1818,11 +1875,12 @@ void WebServer::on_event(event::Event *obj) { void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (event::Event *obj : App.get_events()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; // Note: request->method() is always HTTP_GET here (canHandle ensures this) - if (match.method_empty()) { + if (entity_match.action_is_empty) { auto detail = get_request_detail(request); std::string data = this->event_json_(obj, "", detail); request->send(200, "application/json", data.c_str()); @@ -1887,10 +1945,11 @@ void WebServer::on_update(update::UpdateEntity *obj) { } void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (update::UpdateEntity *obj : App.get_updates()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); std::string data = this->update_json_(obj, detail); request->send(200, "application/json", data.c_str()); diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 78d19fdb7e..b443419351 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -36,6 +36,12 @@ extern const size_t ESPHOME_WEBSERVER_JS_INCLUDE_SIZE; namespace esphome { namespace web_server { +/// Result of matching a URL against an entity +struct EntityMatchResult { + bool matched; ///< True if entity matched the URL + bool action_is_empty; ///< True if no action in URL (or action field was used as entity name for 2-seg subdevice) +}; + /// Internal helper struct that is used to parse incoming URLs /// Note: Length fields use uint8_t, so NAME_MAX_LENGTH in config_validation.py must stay < 255 struct UrlMatch { @@ -50,7 +56,6 @@ struct UrlMatch { uint8_t method_len; ///< Length of method string #ifdef USE_DEVICES uint8_t device_name_len; ///< Length of device name string (NAME_MAX_LENGTH must be < 255) - mutable bool matched_as_two_segment_subdevice{false}; ///< Set when 2-segment URL matched as device/entity #endif bool valid; ///< Whether this match is valid @@ -62,77 +67,13 @@ struct UrlMatch { /// Check if URL id segment matches a string (by pointer and length) bool id_matches(const char *str, size_t len) const { return id && id_len == len && memcmp(id, str, len) == 0; } - /// Match entity by name first, then fall back to object_id for backward compatibility - /// Returns true if entity matches. Sets used_deprecated_format to true if matched via object_id. - bool matches_entity(EntityBase *entity, bool &used_deprecated_format) const { - used_deprecated_format = false; - -#ifdef USE_DEVICES - // Check device match - Device *entity_device = entity->get_device(); - bool url_has_device = (device_name_len > 0); - bool entity_has_device = (entity_device != nullptr); - - if (url_has_device) { - // URL has explicit device segment - must match - if (!entity_has_device) { - return false; // URL has device but entity doesn't - } - const char *entity_device_name = entity_device->get_name(); - if (device_name_len != strlen(entity_device_name) || - memcmp(device_name, entity_device_name, device_name_len) != 0) { - return false; // Device name doesn't match - } - } else if (entity_has_device && method_len > 0) { - // URL has 2 segments (id/method), entity has device - // Try interpreting as device/entity: id=device_name, method=entity_name - const char *entity_device_name = entity_device->get_name(); - if (id_len == strlen(entity_device_name) && memcmp(id, entity_device_name, id_len) == 0) { - // Device name matches, check if method matches entity name - const StringRef &name_ref = entity->get_name(); - if (method_len == name_ref.size() && memcmp(method, name_ref.c_str(), method_len) == 0) { - matched_as_two_segment_subdevice = true; // Mark for method_empty() check - return true; // Matched as device/entity (2-segment sub-device URL) - } - } - return false; // Entity has device but URL doesn't match as device/entity - } else if (entity_has_device) { - return false; // Entity has device but URL has no device info - } -#endif - - // Try matching by entity name first (new format) - const StringRef &name_ref = entity->get_name(); - if (id_matches(name_ref.c_str(), name_ref.size())) { - return true; - } - - // Fall back to object_id (deprecated format) - char object_id_buf[OBJECT_ID_MAX_LEN]; - StringRef object_id = entity->get_object_id_to(object_id_buf); - if (id_matches(object_id.c_str(), object_id.size())) { - used_deprecated_format = true; - return true; - } - - return false; - } - /// Match entity by name first, then fall back to object_id with deprecation warning - bool id_equals_entity(EntityBase *entity) const; + /// Returns EntityMatchResult with match status and whether method is effectively empty + EntityMatchResult match_entity(EntityBase *entity) const; bool method_equals(const char *str) const { return method && method_len == strlen(str) && memcmp(method, str, method_len) == 0; } - - bool method_empty() const { -#ifdef USE_DEVICES - // For 2-segment sub-device URLs, method field contains entity name, not actual method - if (matched_as_two_segment_subdevice) - return true; -#endif - return method_len == 0; - } }; #ifdef USE_WEBSERVER_SORTING