1
0
mirror of https://github.com/esphome/esphome.git synced 2026-02-08 08:41:59 +00:00

refactor to reduce complexity

This commit is contained in:
J. Nick Koston
2025-12-22 17:22:14 -10:00
parent d1ff959f4c
commit ddd43f466a
2 changed files with 114 additions and 114 deletions

View File

@@ -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());

View File

@@ -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