From 9890659f61cd2af1c54c9c1b9f8da06e4ebc1bca Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Jun 2025 23:12:03 -0500 Subject: [PATCH 1/5] Optimize web_server UrlMatch to avoid heap allocations (#9263) --- esphome/components/web_server/web_server.cpp | 287 +++++++++++-------- esphome/components/web_server/web_server.h | 24 +- 2 files changed, 186 insertions(+), 125 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 1bf3ed11cb..669bfbf279 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -49,26 +49,69 @@ static const char *const HEADER_CORS_ALLOW_PNA = "Access-Control-Allow-Private-N UrlMatch match_url(const std::string &url, bool only_domain = false) { UrlMatch match; match.valid = false; - size_t domain_end = url.find('/', 1); - if (domain_end == std::string::npos) + match.domain = nullptr; + match.id = nullptr; + match.method = nullptr; + match.domain_len = 0; + match.id_len = 0; + match.method_len = 0; + + const char *url_ptr = url.c_str(); + size_t url_len = url.length(); + + // URL must start with '/' + if (url_len < 2 || url_ptr[0] != '/') return match; - match.domain = url.substr(1, domain_end - 1); + + // Find domain + size_t domain_start = 1; + size_t domain_end = url.find('/', domain_start); + + if (domain_end == std::string::npos) { + // URL is just "/domain" + match.domain = url_ptr + domain_start; + match.domain_len = url_len - domain_start; + match.valid = true; + return match; + } + + // Set domain + match.domain = url_ptr + domain_start; + match.domain_len = domain_end - domain_start; + if (only_domain) { match.valid = true; return match; } - if (url.length() == domain_end - 1) + + // Check if there's anything after domain + if (url_len == domain_end + 1) return match; + + // Find ID size_t id_begin = domain_end + 1; size_t id_end = url.find('/', id_begin); + match.valid = true; + if (id_end == std::string::npos) { - match.id = url.substr(id_begin, url.length() - id_begin); + // URL is "/domain/id" with no method + match.id = url_ptr + id_begin; + match.id_len = url_len - id_begin; return match; } - match.id = url.substr(id_begin, id_end - id_begin); + + // Set ID + match.id = url_ptr + id_begin; + match.id_len = id_end - id_begin; + + // Set method if present size_t method_begin = id_end + 1; - match.method = url.substr(method_begin, url.length() - method_begin); + if (method_begin < url_len) { + match.method = url_ptr + method_begin; + match.method_len = url_len - method_begin; + } + return match; } @@ -386,9 +429,9 @@ void WebServer::on_sensor_update(sensor::Sensor *obj, float state) { } void WebServer::handle_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (sensor::Sensor *obj : App.get_sensors()) { - if (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { + if (request->method() == HTTP_GET && match.method_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()); @@ -431,9 +474,9 @@ void WebServer::on_text_sensor_update(text_sensor::TextSensor *obj, const std::s } void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (text_sensor::TextSensor *obj : App.get_text_sensors()) { - if (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { + if (request->method() == HTTP_GET && match.method_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()); @@ -469,20 +512,20 @@ void WebServer::on_switch_update(switch_::Switch *obj, bool state) { } void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (switch_::Switch *obj : App.get_switches()) { - if (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { + if (request->method() == HTTP_GET && match.method_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()); - } else if (match.method == "toggle") { + } else if (match.method_equals("toggle")) { this->schedule_([obj]() { obj->toggle(); }); request->send(200); - } else if (match.method == "turn_on") { + } else if (match.method_equals("turn_on")) { this->schedule_([obj]() { obj->turn_on(); }); request->send(200); - } else if (match.method == "turn_off") { + } else if (match.method_equals("turn_off")) { this->schedule_([obj]() { obj->turn_off(); }); request->send(200); } else { @@ -512,13 +555,13 @@ 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 (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { + if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); std::string data = this->button_json(obj, detail); request->send(200, "application/json", data.c_str()); - } else if (match.method == "press") { + } else if (match.method_equals("press")) { this->schedule_([obj]() { obj->press(); }); request->send(200); return; @@ -553,9 +596,9 @@ 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 (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { + if (request->method() == HTTP_GET && match.method_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()); @@ -591,18 +634,18 @@ 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 (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { + if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); std::string data = this->fan_json(obj, detail); request->send(200, "application/json", data.c_str()); - } else if (match.method == "toggle") { + } else if (match.method_equals("toggle")) { this->schedule_([obj]() { obj->toggle().perform(); }); request->send(200); - } else if (match.method == "turn_on" || match.method == "turn_off") { - auto call = match.method == "turn_on" ? obj->turn_on() : obj->turn_off(); + } else if (match.method_equals("turn_on") || match.method_equals("turn_off")) { + auto call = match.method_equals("turn_on") ? obj->turn_on() : obj->turn_off(); if (request->hasParam("speed_level")) { auto speed_level = request->getParam("speed_level")->value(); @@ -672,17 +715,17 @@ 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 (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { + if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); std::string data = this->light_json(obj, detail); request->send(200, "application/json", data.c_str()); - } else if (match.method == "toggle") { + } else if (match.method_equals("toggle")) { this->schedule_([obj]() { obj->toggle().perform(); }); request->send(200); - } else if (match.method == "turn_on") { + } else if (match.method_equals("turn_on")) { auto call = obj->turn_on(); if (request->hasParam("brightness")) { auto brightness = parse_number(request->getParam("brightness")->value().c_str()); @@ -739,7 +782,7 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa this->schedule_([call]() mutable { call.perform(); }); request->send(200); - } else if (match.method == "turn_off") { + } else if (match.method_equals("turn_off")) { auto call = obj->turn_off(); if (request->hasParam("transition")) { auto transition = parse_number(request->getParam("transition")->value().c_str()); @@ -788,10 +831,10 @@ 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 (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { + if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); std::string data = this->cover_json(obj, detail); request->send(200, "application/json", data.c_str()); @@ -799,15 +842,15 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa } auto call = obj->make_call(); - if (match.method == "open") { + if (match.method_equals("open")) { call.set_command_open(); - } else if (match.method == "close") { + } else if (match.method_equals("close")) { call.set_command_close(); - } else if (match.method == "stop") { + } else if (match.method_equals("stop")) { call.set_command_stop(); - } else if (match.method == "toggle") { + } else if (match.method_equals("toggle")) { call.set_command_toggle(); - } else if (match.method != "set") { + } else if (!match.method_equals("set")) { request->send(404); return; } @@ -869,16 +912,16 @@ void WebServer::on_number_update(number::Number *obj, float state) { } void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_numbers()) { - if (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { + if (request->method() == HTTP_GET && match.method_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()); return; } - if (match.method != "set") { + if (!match.method_equals("set")) { request->send(404); return; } @@ -940,15 +983,15 @@ 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 (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { + if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); std::string data = this->date_json(obj, detail); request->send(200, "application/json", data.c_str()); return; } - if (match.method != "set") { + if (!match.method_equals("set")) { request->send(404); return; } @@ -999,15 +1042,15 @@ 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 (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { + if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); std::string data = this->time_json(obj, detail); request->send(200, "application/json", data.c_str()); return; } - if (match.method != "set") { + if (!match.method_equals("set")) { request->send(404); return; } @@ -1057,15 +1100,15 @@ 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 (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { + if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); std::string data = this->datetime_json(obj, detail); request->send(200, "application/json", data.c_str()); return; } - if (match.method != "set") { + if (!match.method_equals("set")) { request->send(404); return; } @@ -1116,16 +1159,16 @@ void WebServer::on_text_update(text::Text *obj, const std::string &state) { } void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_texts()) { - if (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { + if (request->method() == HTTP_GET && match.method_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()); return; } - if (match.method != "set") { + if (!match.method_equals("set")) { request->send(404); return; } @@ -1177,17 +1220,17 @@ void WebServer::on_select_update(select::Select *obj, const std::string &state, } void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_selects()) { - if (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { + if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); std::string data = this->select_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } - if (match.method != "set") { + if (!match.method_equals("set")) { request->send(404); return; } @@ -1236,17 +1279,17 @@ 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 (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { + if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); std::string data = this->climate_json(obj, detail); request->send(200, "application/json", data.c_str()); return; } - if (match.method != "set") { + if (!match.method_equals("set")) { request->send(404); return; } @@ -1395,20 +1438,20 @@ 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 (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { + if (request->method() == HTTP_GET && match.method_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()); - } else if (match.method == "lock") { + } else if (match.method_equals("lock")) { this->schedule_([obj]() { obj->lock(); }); request->send(200); - } else if (match.method == "unlock") { + } else if (match.method_equals("unlock")) { this->schedule_([obj]() { obj->unlock(); }); request->send(200); - } else if (match.method == "open") { + } else if (match.method_equals("open")) { this->schedule_([obj]() { obj->open(); }); request->send(200); } else { @@ -1443,10 +1486,10 @@ 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 (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { + if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); std::string data = this->valve_json(obj, detail); request->send(200, "application/json", data.c_str()); @@ -1454,15 +1497,15 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa } auto call = obj->make_call(); - if (match.method == "open") { + if (match.method_equals("open")) { call.set_command_open(); - } else if (match.method == "close") { + } else if (match.method_equals("close")) { call.set_command_close(); - } else if (match.method == "stop") { + } else if (match.method_equals("stop")) { call.set_command_stop(); - } else if (match.method == "toggle") { + } else if (match.method_equals("toggle")) { call.set_command_toggle(); - } else if (match.method != "set") { + } else if (!match.method_equals("set")) { request->send(404); return; } @@ -1515,10 +1558,10 @@ 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 (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { + if (request->method() == HTTP_GET && match.method_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()); @@ -1530,15 +1573,15 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques call.set_code(request->getParam("code")->value().c_str()); // NOLINT } - if (match.method == "disarm") { + if (match.method_equals("disarm")) { call.disarm(); - } else if (match.method == "arm_away") { + } else if (match.method_equals("arm_away")) { call.arm_away(); - } else if (match.method == "arm_home") { + } else if (match.method_equals("arm_home")) { call.arm_home(); - } else if (match.method == "arm_night") { + } else if (match.method_equals("arm_night")) { call.arm_night(); - } else if (match.method == "arm_vacation") { + } else if (match.method_equals("arm_vacation")) { call.arm_vacation(); } else { request->send(404); @@ -1582,10 +1625,10 @@ void WebServer::on_event(event::Event *obj, const std::string &event_type) { void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (event::Event *obj : App.get_events()) { - if (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { + if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); std::string data = this->event_json(obj, "", detail); request->send(200, "application/json", data.c_str()); @@ -1631,17 +1674,17 @@ 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 (obj->get_object_id() != match.id) + if (!match.id_equals(obj->get_object_id())) continue; - if (request->method() == HTTP_GET && match.method.empty()) { + if (request->method() == HTTP_GET && match.method_empty()) { auto detail = get_request_detail(request); std::string data = this->update_json(obj, detail); request->send(200, "application/json", data.c_str()); return; } - if (match.method != "install") { + if (!match.method_equals("install")) { request->send(404); return; } @@ -1717,102 +1760,102 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) const { if (!match.valid) return false; #ifdef USE_SENSOR - if (request->method() == HTTP_GET && match.domain == "sensor") + if (request->method() == HTTP_GET && match.domain_equals("sensor")) return true; #endif #ifdef USE_SWITCH - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "switch") + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("switch")) return true; #endif #ifdef USE_BUTTON - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "button") + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("button")) return true; #endif #ifdef USE_BINARY_SENSOR - if (request->method() == HTTP_GET && match.domain == "binary_sensor") + if (request->method() == HTTP_GET && match.domain_equals("binary_sensor")) return true; #endif #ifdef USE_FAN - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "fan") + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("fan")) return true; #endif #ifdef USE_LIGHT - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "light") + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("light")) return true; #endif #ifdef USE_TEXT_SENSOR - if (request->method() == HTTP_GET && match.domain == "text_sensor") + if (request->method() == HTTP_GET && match.domain_equals("text_sensor")) return true; #endif #ifdef USE_COVER - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "cover") + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("cover")) return true; #endif #ifdef USE_NUMBER - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "number") + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("number")) return true; #endif #ifdef USE_DATETIME_DATE - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "date") + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("date")) return true; #endif #ifdef USE_DATETIME_TIME - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "time") + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("time")) return true; #endif #ifdef USE_DATETIME_DATETIME - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "datetime") + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("datetime")) return true; #endif #ifdef USE_TEXT - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "text") + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("text")) return true; #endif #ifdef USE_SELECT - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "select") + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("select")) return true; #endif #ifdef USE_CLIMATE - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "climate") + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("climate")) return true; #endif #ifdef USE_LOCK - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "lock") + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("lock")) return true; #endif #ifdef USE_VALVE - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "valve") + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("valve")) return true; #endif #ifdef USE_ALARM_CONTROL_PANEL - if ((request->method() == HTTP_GET || request->method() == HTTP_POST) && match.domain == "alarm_control_panel") + if ((request->method() == HTTP_GET || request->method() == HTTP_POST) && match.domain_equals("alarm_control_panel")) return true; #endif #ifdef USE_EVENT - if (request->method() == HTTP_GET && match.domain == "event") + if (request->method() == HTTP_GET && match.domain_equals("event")) return true; #endif #ifdef USE_UPDATE - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "update") + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("update")) return true; #endif @@ -1854,112 +1897,112 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { UrlMatch match = match_url(request->url().c_str()); // NOLINT #ifdef USE_SENSOR - if (match.domain == "sensor") { + if (match.domain_equals("sensor")) { this->handle_sensor_request(request, match); return; } #endif #ifdef USE_SWITCH - if (match.domain == "switch") { + if (match.domain_equals("switch")) { this->handle_switch_request(request, match); return; } #endif #ifdef USE_BUTTON - if (match.domain == "button") { + if (match.domain_equals("button")) { this->handle_button_request(request, match); return; } #endif #ifdef USE_BINARY_SENSOR - if (match.domain == "binary_sensor") { + if (match.domain_equals("binary_sensor")) { this->handle_binary_sensor_request(request, match); return; } #endif #ifdef USE_FAN - if (match.domain == "fan") { + if (match.domain_equals("fan")) { this->handle_fan_request(request, match); return; } #endif #ifdef USE_LIGHT - if (match.domain == "light") { + if (match.domain_equals("light")) { this->handle_light_request(request, match); return; } #endif #ifdef USE_TEXT_SENSOR - if (match.domain == "text_sensor") { + if (match.domain_equals("text_sensor")) { this->handle_text_sensor_request(request, match); return; } #endif #ifdef USE_COVER - if (match.domain == "cover") { + if (match.domain_equals("cover")) { this->handle_cover_request(request, match); return; } #endif #ifdef USE_NUMBER - if (match.domain == "number") { + if (match.domain_equals("number")) { this->handle_number_request(request, match); return; } #endif #ifdef USE_DATETIME_DATE - if (match.domain == "date") { + if (match.domain_equals("date")) { this->handle_date_request(request, match); return; } #endif #ifdef USE_DATETIME_TIME - if (match.domain == "time") { + if (match.domain_equals("time")) { this->handle_time_request(request, match); return; } #endif #ifdef USE_DATETIME_DATETIME - if (match.domain == "datetime") { + if (match.domain_equals("datetime")) { this->handle_datetime_request(request, match); return; } #endif #ifdef USE_TEXT - if (match.domain == "text") { + if (match.domain_equals("text")) { this->handle_text_request(request, match); return; } #endif #ifdef USE_SELECT - if (match.domain == "select") { + if (match.domain_equals("select")) { this->handle_select_request(request, match); return; } #endif #ifdef USE_CLIMATE - if (match.domain == "climate") { + if (match.domain_equals("climate")) { this->handle_climate_request(request, match); return; } #endif #ifdef USE_LOCK - if (match.domain == "lock") { + if (match.domain_equals("lock")) { this->handle_lock_request(request, match); return; @@ -1967,14 +2010,14 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { #endif #ifdef USE_VALVE - if (match.domain == "valve") { + if (match.domain_equals("valve")) { this->handle_valve_request(request, match); return; } #endif #ifdef USE_ALARM_CONTROL_PANEL - if (match.domain == "alarm_control_panel") { + if (match.domain_equals("alarm_control_panel")) { this->handle_alarm_control_panel_request(request, match); return; @@ -1982,7 +2025,7 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { #endif #ifdef USE_UPDATE - if (match.domain == "update") { + if (match.domain_equals("update")) { this->handle_update_request(request, match); return; } diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 3be99eebae..991bca6fa7 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -40,10 +40,28 @@ namespace web_server { /// Internal helper struct that is used to parse incoming URLs struct UrlMatch { - std::string domain; ///< The domain of the component, for example "sensor" - std::string id; ///< The id of the device that's being accessed, for example "living_room_fan" - std::string method; ///< The method that's being called, for example "turn_on" + const char *domain; ///< Pointer to domain within URL, for example "sensor" + const char *id; ///< Pointer to id within URL, for example "living_room_fan" + const char *method; ///< Pointer to method within URL, for example "turn_on" + uint8_t domain_len; ///< Length of domain string + uint8_t id_len; ///< Length of id string + uint8_t method_len; ///< Length of method string bool valid; ///< Whether this match is valid + + // Helper methods for string comparisons + bool domain_equals(const char *str) const { + return domain && domain_len == strlen(str) && memcmp(domain, str, domain_len) == 0; + } + + bool id_equals(const std::string &str) const { + return id && id_len == str.length() && memcmp(id, str.c_str(), id_len) == 0; + } + + bool method_equals(const char *str) const { + return method && method_len == strlen(str) && memcmp(method, str, method_len) == 0; + } + + bool method_empty() const { return method_len == 0; } }; #ifdef USE_WEBSERVER_SORTING From d86f319d66918c0bcb729ad41801fda0f7a9db8c Mon Sep 17 00:00:00 2001 From: lamauny <57617527+lamauny@users.noreply.github.com> Date: Mon, 30 Jun 2025 06:20:36 +0200 Subject: [PATCH 2/5] Add support for LN882X Family (with LibreTiny) (#8954) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/__main__.py | 4 +- esphome/components/api/api_connection.cpp | 2 + esphome/components/async_tcp/__init__.py | 11 +- esphome/components/captive_portal/__init__.py | 11 +- esphome/components/esphome/ota/__init__.py | 1 + esphome/components/libretiny/const.py | 5 + .../libretiny/generate_components.py | 1 + esphome/components/ln882x/__init__.py | 52 ++++ esphome/components/ln882x/boards.py | 285 ++++++++++++++++++ esphome/components/logger/__init__.py | 10 +- .../components/remote_receiver/__init__.py | 1 + esphome/components/sntp/time.py | 2 + esphome/components/socket/__init__.py | 1 + esphome/components/web_server/__init__.py | 12 +- esphome/components/wifi/__init__.py | 1 + esphome/const.py | 1 + esphome/core/__init__.py | 7 +- esphome/dashboard/web_server.py | 10 +- esphome/wizard.py | 19 +- platformio.ini | 13 +- tests/components/adc/test.ln882x-ard.yaml | 4 + .../binary_sensor/test.ln882x-ard.yaml | 2 + tests/components/debug/test.ln882x-ard.yaml | 1 + .../homeassistant/test.ln882x-ard.yaml | 2 + tests/components/script/test.ln882x-ard.yaml | 1 + tests/components/sntp/test.ln882x-ard.yaml | 1 + tests/components/switch/test.ln882x-ard.yaml | 2 + tests/components/syslog/test.ln882x-ard.yaml | 1 + .../components/template/test.ln882x-ard.yaml | 2 + .../build_components_base.ln882x-ard.yaml | 15 + tests/unit_tests/test_config_validation.py | 7 +- tests/unit_tests/test_wizard.py | 22 ++ 33 files changed, 495 insertions(+), 15 deletions(-) create mode 100644 esphome/components/ln882x/__init__.py create mode 100644 esphome/components/ln882x/boards.py create mode 100644 tests/components/adc/test.ln882x-ard.yaml create mode 100644 tests/components/binary_sensor/test.ln882x-ard.yaml create mode 100644 tests/components/debug/test.ln882x-ard.yaml create mode 100644 tests/components/homeassistant/test.ln882x-ard.yaml create mode 100644 tests/components/script/test.ln882x-ard.yaml create mode 100644 tests/components/sntp/test.ln882x-ard.yaml create mode 100644 tests/components/switch/test.ln882x-ard.yaml create mode 100644 tests/components/syslog/test.ln882x-ard.yaml create mode 100644 tests/components/template/test.ln882x-ard.yaml create mode 100644 tests/test_build_components/build_components_base.ln882x-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index b3c66c775b..68c8684024 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -248,6 +248,7 @@ esphome/components/libretiny_pwm/* @kuba2k2 esphome/components/light/* @esphome/core esphome/components/lightwaverf/* @max246 esphome/components/lilygo_t5_47/touchscreen/* @jesserockz +esphome/components/ln882x/* @lamauny esphome/components/lock/* @esphome/core esphome/components/logger/* @esphome/core esphome/components/logger/select/* @clydebarrow diff --git a/esphome/__main__.py b/esphome/__main__.py index 2dbdfeb1ff..d8a79c018a 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -34,11 +34,9 @@ from esphome.const import ( CONF_PORT, CONF_SUBSTITUTIONS, CONF_TOPIC, - PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040, - PLATFORM_RTL87XX, SECRETS_FILES, ) from esphome.core import CORE, EsphomeError, coroutine @@ -354,7 +352,7 @@ def upload_program(config, args, host): if CORE.target_platform in (PLATFORM_RP2040): return upload_using_platformio(config, args.device) - if CORE.target_platform in (PLATFORM_BK72XX, PLATFORM_RTL87XX): + if CORE.is_libretiny: return upload_using_platformio(config, host) return 1 # Unknown target platform diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 6a40f21f99..b7624221c9 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1537,6 +1537,8 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { resp.manufacturer = "Raspberry Pi"; #elif defined(USE_BK72XX) resp.manufacturer = "Beken"; +#elif defined(USE_LN882X) + resp.manufacturer = "Lightning"; #elif defined(USE_RTL87XX) resp.manufacturer = "Realtek"; #elif defined(USE_HOST) diff --git a/esphome/components/async_tcp/__init__.py b/esphome/components/async_tcp/__init__.py index eec6a0e327..29097ce1b6 100644 --- a/esphome/components/async_tcp/__init__.py +++ b/esphome/components/async_tcp/__init__.py @@ -5,6 +5,7 @@ from esphome.const import ( PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, + PLATFORM_LN882X, PLATFORM_RTL87XX, ) from esphome.core import CORE, coroutine_with_priority @@ -14,7 +15,15 @@ CODEOWNERS = ["@OttoWinter"] CONFIG_SCHEMA = cv.All( cv.Schema({}), cv.only_with_arduino, - cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX, PLATFORM_RTL87XX]), + cv.only_on( + [ + PLATFORM_ESP32, + PLATFORM_ESP8266, + PLATFORM_BK72XX, + PLATFORM_LN882X, + PLATFORM_RTL87XX, + ] + ), ) diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index a55887948d..cba3b4921a 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -7,6 +7,7 @@ from esphome.const import ( PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, + PLATFORM_LN882X, PLATFORM_RTL87XX, ) from esphome.core import CORE, coroutine_with_priority @@ -27,7 +28,15 @@ CONFIG_SCHEMA = cv.All( ), } ).extend(cv.COMPONENT_SCHEMA), - cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX, PLATFORM_RTL87XX]), + cv.only_on( + [ + PLATFORM_ESP32, + PLATFORM_ESP8266, + PLATFORM_BK72XX, + PLATFORM_LN882X, + PLATFORM_RTL87XX, + ] + ), ) diff --git a/esphome/components/esphome/ota/__init__.py b/esphome/components/esphome/ota/__init__.py index 86006e3e18..901657ec82 100644 --- a/esphome/components/esphome/ota/__init__.py +++ b/esphome/components/esphome/ota/__init__.py @@ -100,6 +100,7 @@ CONFIG_SCHEMA = ( esp32=3232, rp2040=2040, bk72xx=8892, + ln882x=8820, rtl87xx=8892, ): cv.port, cv.Optional(CONF_PASSWORD): cv.string, diff --git a/esphome/components/libretiny/const.py b/esphome/components/libretiny/const.py index 362609df44..671992f8bd 100644 --- a/esphome/components/libretiny/const.py +++ b/esphome/components/libretiny/const.py @@ -50,6 +50,7 @@ KEY_FAMILY = "family" # COMPONENTS - auto-generated! Do not modify this block. COMPONENT_BK72XX = "bk72xx" +COMPONENT_LN882X = "ln882x" COMPONENT_RTL87XX = "rtl87xx" # COMPONENTS - end @@ -58,6 +59,7 @@ FAMILY_BK7231N = "BK7231N" FAMILY_BK7231Q = "BK7231Q" FAMILY_BK7231T = "BK7231T" FAMILY_BK7251 = "BK7251" +FAMILY_LN882H = "LN882H" FAMILY_RTL8710B = "RTL8710B" FAMILY_RTL8720C = "RTL8720C" FAMILIES = [ @@ -65,6 +67,7 @@ FAMILIES = [ FAMILY_BK7231Q, FAMILY_BK7231T, FAMILY_BK7251, + FAMILY_LN882H, FAMILY_RTL8710B, FAMILY_RTL8720C, ] @@ -73,6 +76,7 @@ FAMILY_FRIENDLY = { FAMILY_BK7231Q: "BK7231Q", FAMILY_BK7231T: "BK7231T", FAMILY_BK7251: "BK7251", + FAMILY_LN882H: "LN882H", FAMILY_RTL8710B: "RTL8710B", FAMILY_RTL8720C: "RTL8720C", } @@ -81,6 +85,7 @@ FAMILY_COMPONENT = { FAMILY_BK7231Q: COMPONENT_BK72XX, FAMILY_BK7231T: COMPONENT_BK72XX, FAMILY_BK7251: COMPONENT_BK72XX, + FAMILY_LN882H: COMPONENT_LN882X, FAMILY_RTL8710B: COMPONENT_RTL87XX, FAMILY_RTL8720C: COMPONENT_RTL87XX, } diff --git a/esphome/components/libretiny/generate_components.py b/esphome/components/libretiny/generate_components.py index ae55fd9e40..c750b79317 100644 --- a/esphome/components/libretiny/generate_components.py +++ b/esphome/components/libretiny/generate_components.py @@ -94,6 +94,7 @@ PIN_SCHEMA_EXTRA = f"libretiny.BASE_PIN_SCHEMA.extend({VAR_PIN_SCHEMA})" COMPONENT_MAP = { "rtl87xx": "realtek-amb", "bk72xx": "beken-72xx", + "ln882x": "lightning-ln882x", } diff --git a/esphome/components/ln882x/__init__.py b/esphome/components/ln882x/__init__.py new file mode 100644 index 0000000000..6a76218f87 --- /dev/null +++ b/esphome/components/ln882x/__init__.py @@ -0,0 +1,52 @@ +# This file was auto-generated by libretiny/generate_components.py +# Do not modify its contents. +# For custom pin validators, put validate_pin() or validate_usage() +# in gpio.py file in this directory. +# For changing schema/pin schema, put COMPONENT_SCHEMA or COMPONENT_PIN_SCHEMA +# in schema.py file in this directory. + +from esphome import pins +from esphome.components import libretiny +from esphome.components.libretiny.const import ( + COMPONENT_LN882X, + KEY_COMPONENT_DATA, + KEY_LIBRETINY, + LibreTinyComponent, +) +from esphome.core import CORE + +from .boards import LN882X_BOARD_PINS, LN882X_BOARDS + +CODEOWNERS = ["@lamauny"] +AUTO_LOAD = ["libretiny"] +IS_TARGET_PLATFORM = True + +COMPONENT_DATA = LibreTinyComponent( + name=COMPONENT_LN882X, + boards=LN882X_BOARDS, + board_pins=LN882X_BOARD_PINS, + pin_validation=None, + usage_validation=None, +) + + +def _set_core_data(config): + CORE.data[KEY_LIBRETINY] = {} + CORE.data[KEY_LIBRETINY][KEY_COMPONENT_DATA] = COMPONENT_DATA + return config + + +CONFIG_SCHEMA = libretiny.BASE_SCHEMA + +PIN_SCHEMA = libretiny.gpio.BASE_PIN_SCHEMA + +CONFIG_SCHEMA.prepend_extra(_set_core_data) + + +async def to_code(config): + return await libretiny.component_to_code(config) + + +@pins.PIN_SCHEMA_REGISTRY.register("ln882x", PIN_SCHEMA) +async def pin_to_code(config): + return await libretiny.gpio.component_pin_to_code(config) diff --git a/esphome/components/ln882x/boards.py b/esphome/components/ln882x/boards.py new file mode 100644 index 0000000000..43f25994a7 --- /dev/null +++ b/esphome/components/ln882x/boards.py @@ -0,0 +1,285 @@ +# This file was auto-generated by libretiny/generate_components.py +# Do not modify its contents. + +from esphome.components.libretiny.const import FAMILY_LN882H + +LN882X_BOARDS = { + "wl2s": { + "name": "WL2S Wi-Fi/BLE Module", + "family": FAMILY_LN882H, + }, + "ln-02": { + "name": "LN-02 Wi-Fi/BLE Module", + "family": FAMILY_LN882H, + }, + "generic-ln882hki": { + "name": "Generic - LN882HKI", + "family": FAMILY_LN882H, + }, +} + +LN882X_BOARD_PINS = { + "wl2s": { + "WIRE0_SCL_0": 7, + "WIRE0_SCL_1": 12, + "WIRE0_SCL_2": 3, + "WIRE0_SCL_3": 10, + "WIRE0_SCL_4": 2, + "WIRE0_SCL_5": 0, + "WIRE0_SCL_6": 19, + "WIRE0_SCL_7": 11, + "WIRE0_SCL_8": 9, + "WIRE0_SCL_9": 24, + "WIRE0_SCL_10": 25, + "WIRE0_SCL_11": 5, + "WIRE0_SCL_12": 1, + "WIRE0_SDA_0": 7, + "WIRE0_SDA_1": 12, + "WIRE0_SDA_2": 3, + "WIRE0_SDA_3": 10, + "WIRE0_SDA_4": 2, + "WIRE0_SDA_5": 0, + "WIRE0_SDA_6": 19, + "WIRE0_SDA_7": 11, + "WIRE0_SDA_8": 9, + "WIRE0_SDA_9": 24, + "WIRE0_SDA_10": 25, + "WIRE0_SDA_11": 5, + "WIRE0_SDA_12": 1, + "SERIAL0_RX": 3, + "SERIAL0_TX": 2, + "SERIAL1_RX": 24, + "SERIAL1_TX": 25, + "ADC2": 0, + "ADC3": 1, + "ADC5": 19, + "PA00": 0, + "PA0": 0, + "PA01": 1, + "PA1": 1, + "PA02": 2, + "PA2": 2, + "PA03": 3, + "PA3": 3, + "PA05": 5, + "PA5": 5, + "PA07": 7, + "PA7": 7, + "PA09": 9, + "PA9": 9, + "PA10": 10, + "PA11": 11, + "PA12": 12, + "PB03": 19, + "PB3": 19, + "PB08": 24, + "PB8": 24, + "PB09": 25, + "PB9": 25, + "RX0": 3, + "RX1": 24, + "SCL0": 1, + "SDA0": 1, + "TX0": 2, + "TX1": 25, + "D0": 7, + "D1": 12, + "D2": 3, + "D3": 10, + "D4": 2, + "D5": 0, + "D6": 19, + "D7": 11, + "D8": 9, + "D9": 24, + "D10": 25, + "D11": 5, + "D12": 1, + "A0": 0, + "A1": 19, + "A2": 1, + }, + "ln-02": { + "WIRE0_SCL_0": 11, + "WIRE0_SCL_1": 19, + "WIRE0_SCL_2": 3, + "WIRE0_SCL_3": 24, + "WIRE0_SCL_4": 2, + "WIRE0_SCL_5": 25, + "WIRE0_SCL_6": 1, + "WIRE0_SCL_7": 0, + "WIRE0_SCL_8": 9, + "WIRE0_SDA_0": 11, + "WIRE0_SDA_1": 19, + "WIRE0_SDA_2": 3, + "WIRE0_SDA_3": 24, + "WIRE0_SDA_4": 2, + "WIRE0_SDA_5": 25, + "WIRE0_SDA_6": 1, + "WIRE0_SDA_7": 0, + "WIRE0_SDA_8": 9, + "SERIAL0_RX": 3, + "SERIAL0_TX": 2, + "SERIAL1_RX": 24, + "SERIAL1_TX": 25, + "ADC2": 0, + "ADC3": 1, + "ADC5": 19, + "PA00": 0, + "PA0": 0, + "PA01": 1, + "PA1": 1, + "PA02": 2, + "PA2": 2, + "PA03": 3, + "PA3": 3, + "PA09": 9, + "PA9": 9, + "PA11": 11, + "PB03": 19, + "PB3": 19, + "PB08": 24, + "PB8": 24, + "PB09": 25, + "PB9": 25, + "RX0": 3, + "RX1": 24, + "SCL0": 9, + "SDA0": 9, + "TX0": 2, + "TX1": 25, + "D0": 11, + "D1": 19, + "D2": 3, + "D3": 24, + "D4": 2, + "D5": 25, + "D6": 1, + "D7": 0, + "D8": 9, + "A0": 19, + "A1": 1, + "A2": 0, + }, + "generic-ln882hki": { + "WIRE0_SCL_0": 0, + "WIRE0_SCL_1": 1, + "WIRE0_SCL_2": 2, + "WIRE0_SCL_3": 3, + "WIRE0_SCL_4": 4, + "WIRE0_SCL_5": 5, + "WIRE0_SCL_6": 6, + "WIRE0_SCL_7": 7, + "WIRE0_SCL_8": 8, + "WIRE0_SCL_9": 9, + "WIRE0_SCL_10": 10, + "WIRE0_SCL_11": 11, + "WIRE0_SCL_12": 12, + "WIRE0_SCL_13": 19, + "WIRE0_SCL_14": 20, + "WIRE0_SCL_15": 21, + "WIRE0_SCL_16": 22, + "WIRE0_SCL_17": 23, + "WIRE0_SCL_18": 24, + "WIRE0_SCL_19": 25, + "WIRE0_SDA_0": 0, + "WIRE0_SDA_1": 1, + "WIRE0_SDA_2": 2, + "WIRE0_SDA_3": 3, + "WIRE0_SDA_4": 4, + "WIRE0_SDA_5": 5, + "WIRE0_SDA_6": 6, + "WIRE0_SDA_7": 7, + "WIRE0_SDA_8": 8, + "WIRE0_SDA_9": 9, + "WIRE0_SDA_10": 10, + "WIRE0_SDA_11": 11, + "WIRE0_SDA_12": 12, + "WIRE0_SDA_13": 19, + "WIRE0_SDA_14": 20, + "WIRE0_SDA_15": 21, + "WIRE0_SDA_16": 22, + "WIRE0_SDA_17": 23, + "WIRE0_SDA_18": 24, + "WIRE0_SDA_19": 25, + "SERIAL0_RX": 3, + "SERIAL0_TX": 2, + "SERIAL1_RX": 24, + "SERIAL1_TX": 25, + "ADC2": 0, + "ADC3": 1, + "ADC4": 4, + "ADC5": 19, + "ADC6": 20, + "ADC7": 21, + "PA00": 0, + "PA0": 0, + "PA01": 1, + "PA1": 1, + "PA02": 2, + "PA2": 2, + "PA03": 3, + "PA3": 3, + "PA04": 4, + "PA4": 4, + "PA05": 5, + "PA5": 5, + "PA06": 6, + "PA6": 6, + "PA07": 7, + "PA7": 7, + "PA08": 8, + "PA8": 8, + "PA09": 9, + "PA9": 9, + "PA10": 10, + "PA11": 11, + "PA12": 12, + "PB03": 19, + "PB3": 19, + "PB04": 20, + "PB4": 20, + "PB05": 21, + "PB5": 21, + "PB06": 22, + "PB6": 22, + "PB07": 23, + "PB7": 23, + "PB08": 24, + "PB8": 24, + "PB09": 25, + "PB9": 25, + "RX0": 3, + "RX1": 24, + "TX0": 2, + "TX1": 25, + "D0": 0, + "D1": 1, + "D2": 2, + "D3": 3, + "D4": 4, + "D5": 5, + "D6": 6, + "D7": 7, + "D8": 8, + "D9": 9, + "D10": 10, + "D11": 11, + "D12": 12, + "D13": 19, + "D14": 20, + "D15": 21, + "D16": 22, + "D17": 23, + "D18": 24, + "D19": 25, + "A2": 0, + "A3": 1, + "A4": 4, + "A5": 19, + "A6": 20, + "A7": 21, + }, +} + +BOARDS = LN882X_BOARDS diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index af62d8a73f..3d4907aa6e 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -16,7 +16,11 @@ from esphome.components.esp32.const import ( VARIANT_ESP32S3, ) from esphome.components.libretiny import get_libretiny_component, get_libretiny_family -from esphome.components.libretiny.const import COMPONENT_BK72XX, COMPONENT_RTL87XX +from esphome.components.libretiny.const import ( + COMPONENT_BK72XX, + COMPONENT_LN882X, + COMPONENT_RTL87XX, +) import esphome.config_validation as cv from esphome.const import ( CONF_ARGS, @@ -35,6 +39,7 @@ from esphome.const import ( PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, + PLATFORM_LN882X, PLATFORM_RP2040, PLATFORM_RTL87XX, ) @@ -100,6 +105,7 @@ UART_SELECTION_ESP8266 = [UART0, UART0_SWAP, UART1] UART_SELECTION_LIBRETINY = { COMPONENT_BK72XX: [DEFAULT, UART1, UART2], + COMPONENT_LN882X: [DEFAULT, UART0, UART1, UART2], COMPONENT_RTL87XX: [DEFAULT, UART0, UART1, UART2], } @@ -217,6 +223,7 @@ CONFIG_SCHEMA = cv.All( esp32_p4_idf=USB_SERIAL_JTAG, rp2040=USB_CDC, bk72xx=DEFAULT, + ln882x=DEFAULT, rtl87xx=DEFAULT, ): cv.All( cv.only_on( @@ -225,6 +232,7 @@ CONFIG_SCHEMA = cv.All( PLATFORM_ESP32, PLATFORM_RP2040, PLATFORM_BK72XX, + PLATFORM_LN882X, PLATFORM_RTL87XX, ] ), diff --git a/esphome/components/remote_receiver/__init__.py b/esphome/components/remote_receiver/__init__.py index 321cfc93ff..5de7d8c9c4 100644 --- a/esphome/components/remote_receiver/__init__.py +++ b/esphome/components/remote_receiver/__init__.py @@ -94,6 +94,7 @@ CONFIG_SCHEMA = remote_base.validate_triggers( esp32="10000b", esp8266="1000b", bk72xx="1000b", + ln882x="1000b", rtl87xx="1000b", ): cv.validate_bytes, cv.Optional(CONF_FILTER, default="50us"): cv.All( diff --git a/esphome/components/sntp/time.py b/esphome/components/sntp/time.py index 6f883d5bed..1c8ee402ad 100644 --- a/esphome/components/sntp/time.py +++ b/esphome/components/sntp/time.py @@ -7,6 +7,7 @@ from esphome.const import ( PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, + PLATFORM_LN882X, PLATFORM_RP2040, PLATFORM_RTL87XX, ) @@ -33,6 +34,7 @@ CONFIG_SCHEMA = cv.All( PLATFORM_ESP8266, PLATFORM_RP2040, PLATFORM_BK72XX, + PLATFORM_LN882X, PLATFORM_RTL87XX, ] ), diff --git a/esphome/components/socket/__init__.py b/esphome/components/socket/__init__.py index 667e30df4b..26031a8da5 100644 --- a/esphome/components/socket/__init__.py +++ b/esphome/components/socket/__init__.py @@ -16,6 +16,7 @@ CONFIG_SCHEMA = cv.Schema( esp32=IMPLEMENTATION_BSD_SOCKETS, rp2040=IMPLEMENTATION_LWIP_TCP, bk72xx=IMPLEMENTATION_LWIP_SOCKETS, + ln882x=IMPLEMENTATION_LWIP_SOCKETS, rtl87xx=IMPLEMENTATION_LWIP_SOCKETS, host=IMPLEMENTATION_BSD_SOCKETS, ): cv.one_of( diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index 8ff7ce1d16..f2c1824028 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -28,6 +28,7 @@ from esphome.const import ( PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, + PLATFORM_LN882X, PLATFORM_RTL87XX, ) from esphome.core import CORE, coroutine_with_priority @@ -180,6 +181,7 @@ CONFIG_SCHEMA = cv.All( esp32_arduino=True, esp32_idf=False, bk72xx=True, + ln882x=True, rtl87xx=True, ): cv.boolean, cv.Optional(CONF_LOG, default=True): cv.boolean, @@ -187,7 +189,15 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_SORTING_GROUPS): cv.ensure_list(sorting_group), } ).extend(cv.COMPONENT_SCHEMA), - cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX, PLATFORM_RTL87XX]), + cv.only_on( + [ + PLATFORM_ESP32, + PLATFORM_ESP8266, + PLATFORM_BK72XX, + PLATFORM_LN882X, + PLATFORM_RTL87XX, + ] + ), default_url, validate_local, validate_ota, diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 582b826de0..e8ae9b1b4e 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -309,6 +309,7 @@ CONFIG_SCHEMA = cv.All( rp2040="light", bk72xx="none", rtl87xx="none", + ln882x="light", ): cv.enum(WIFI_POWER_SAVE_MODES, upper=True), cv.Optional(CONF_FAST_CONNECT, default=False): cv.boolean, cv.Optional(CONF_USE_ADDRESS): cv.string_strict, diff --git a/esphome/const.py b/esphome/const.py index ed6390d8c3..b167935d12 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -12,6 +12,7 @@ PLATFORM_ESP32 = "esp32" PLATFORM_ESP8266 = "esp8266" PLATFORM_HOST = "host" PLATFORM_LIBRETINY_OLDSTYLE = "libretiny" +PLATFORM_LN882X = "ln882x" PLATFORM_RP2040 = "rp2040" PLATFORM_RTL87XX = "rtl87xx" diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 368e2affe9..e33bbcf726 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -20,6 +20,7 @@ from esphome.const import ( PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_HOST, + PLATFORM_LN882X, PLATFORM_RP2040, PLATFORM_RTL87XX, ) @@ -661,9 +662,13 @@ class EsphomeCore: def is_rtl87xx(self): return self.target_platform == PLATFORM_RTL87XX + @property + def is_ln882x(self): + return self.target_platform == PLATFORM_LN882X + @property def is_libretiny(self): - return self.is_bk72xx or self.is_rtl87xx + return self.is_bk72xx or self.is_rtl87xx or self.is_ln882x @property def is_host(self): diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index 529a0815b8..480285b6c1 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -639,7 +639,11 @@ class DownloadListRequestHandler(BaseHandler): if platform.upper() in ESP32_VARIANTS: platform = "esp32" - elif platform in (const.PLATFORM_RTL87XX, const.PLATFORM_BK72XX): + elif platform in ( + const.PLATFORM_RTL87XX, + const.PLATFORM_BK72XX, + const.PLATFORM_LN882X, + ): platform = "libretiny" try: @@ -837,6 +841,10 @@ class BoardsRequestHandler(BaseHandler): from esphome.components.bk72xx.boards import BOARDS as BK72XX_BOARDS boards = BK72XX_BOARDS + elif platform == const.PLATFORM_LN882X: + from esphome.components.ln882x.boards import BOARDS as LN882X_BOARDS + + boards = LN882X_BOARDS elif platform == const.PLATFORM_RTL87XX: from esphome.components.rtl87xx.boards import BOARDS as RTL87XX_BOARDS diff --git a/esphome/wizard.py b/esphome/wizard.py index 7b4d87be63..1826487aa4 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -83,6 +83,11 @@ bk72xx: board: {board} """ +LN882X_CONFIG = """ +ln882x: + board: {board} +""" + RTL87XX_CONFIG = """ rtl87xx: board: {board} @@ -93,6 +98,7 @@ HARDWARE_BASE_CONFIGS = { "ESP32": ESP32_CONFIG, "RP2040": RP2040_CONFIG, "BK72XX": BK72XX_CONFIG, + "LN882X": LN882X_CONFIG, "RTL87XX": RTL87XX_CONFIG, } @@ -157,7 +163,7 @@ def wizard_file(**kwargs): """ # pylint: disable=consider-using-f-string - if kwargs["platform"] in ["ESP8266", "ESP32", "BK72XX", "RTL87XX"]: + if kwargs["platform"] in ["ESP8266", "ESP32", "BK72XX", "LN882X", "RTL87XX"]: config += """ # Enable fallback hotspot (captive portal) in case wifi connection fails ap: @@ -181,6 +187,7 @@ def wizard_write(path, **kwargs): from esphome.components.bk72xx import boards as bk72xx_boards from esphome.components.esp32 import boards as esp32_boards from esphome.components.esp8266 import boards as esp8266_boards + from esphome.components.ln882x import boards as ln882x_boards from esphome.components.rp2040 import boards as rp2040_boards from esphome.components.rtl87xx import boards as rtl87xx_boards @@ -200,6 +207,8 @@ def wizard_write(path, **kwargs): platform = "RP2040" elif board in bk72xx_boards.BOARDS: platform = "BK72XX" + elif board in ln882x_boards.BOARDS: + platform = "LN882X" elif board in rtl87xx_boards.BOARDS: platform = "RTL87XX" else: @@ -253,6 +262,7 @@ def wizard(path): from esphome.components.bk72xx import boards as bk72xx_boards from esphome.components.esp32 import boards as esp32_boards from esphome.components.esp8266 import boards as esp8266_boards + from esphome.components.ln882x import boards as ln882x_boards from esphome.components.rp2040 import boards as rp2040_boards from esphome.components.rtl87xx import boards as rtl87xx_boards @@ -325,7 +335,7 @@ def wizard(path): "firmwares for it." ) - wizard_platforms = ["ESP32", "ESP8266", "BK72XX", "RTL87XX", "RP2040"] + wizard_platforms = ["ESP32", "ESP8266", "BK72XX", "LN882X", "RTL87XX", "RP2040"] safe_print( "Please choose one of the supported microcontrollers " "(Use ESP8266 for Sonoff devices)." @@ -361,7 +371,7 @@ def wizard(path): board_link = ( "https://www.raspberrypi.com/documentation/microcontrollers/rp2040.html" ) - elif platform in ["BK72XX", "RTL87XX"]: + elif platform in ["BK72XX", "LN882X", "RTL87XX"]: board_link = "https://docs.libretiny.eu/docs/status/supported/" else: raise NotImplementedError("Unknown platform!") @@ -384,6 +394,9 @@ def wizard(path): elif platform == "BK72XX": safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "cb2s")}".') boards_list = bk72xx_boards.BOARDS.items() + elif platform == "LN882X": + safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "wl2s")}".') + boards_list = ln882x_boards.BOARDS.items() elif platform == "RTL87XX": safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "wr3")}".') boards_list = rtl87xx_boards.BOARDS.items() diff --git a/platformio.ini b/platformio.ini index be9d7587c2..79e22f90b0 100644 --- a/platformio.ini +++ b/platformio.ini @@ -4,7 +4,7 @@ ; It's *not* used during runtime. [platformio] -default_envs = esp8266-arduino, esp32-arduino, esp32-idf, bk72xx-arduino +default_envs = esp8266-arduino, esp32-arduino, esp32-idf, bk72xx-arduino, ln882h-arduino ; Ideally, we want src_dir to be the root directory of the repository, to mimic the runtime build ; environment as best as possible. Unfortunately, the ESP-IDF toolchain really doesn't like this ; being the root directory. Instead, set esphome/ as the source directory, all our sources are in @@ -530,6 +530,17 @@ build_flags = build_unflags = ${common.build_unflags} +[env:ln882h-arduino] +extends = common:libretiny-arduino +board = generic-ln882hki +build_flags = + ${common:libretiny-arduino.build_flags} + ${flags:runtime.build_flags} + -DUSE_LN882X + -DUSE_LIBRETINY_VARIANT_LN882H +build_unflags = + ${common.build_unflags} + [env:rtl87xxb-arduino] extends = common:libretiny-arduino board = generic-rtl8710bn-2mb-788k diff --git a/tests/components/adc/test.ln882x-ard.yaml b/tests/components/adc/test.ln882x-ard.yaml new file mode 100644 index 0000000000..92c76ca9b3 --- /dev/null +++ b/tests/components/adc/test.ln882x-ard.yaml @@ -0,0 +1,4 @@ +sensor: + - platform: adc + pin: PA0 + name: Basic ADC Test diff --git a/tests/components/binary_sensor/test.ln882x-ard.yaml b/tests/components/binary_sensor/test.ln882x-ard.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/binary_sensor/test.ln882x-ard.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/debug/test.ln882x-ard.yaml b/tests/components/debug/test.ln882x-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/debug/test.ln882x-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/homeassistant/test.ln882x-ard.yaml b/tests/components/homeassistant/test.ln882x-ard.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/homeassistant/test.ln882x-ard.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/script/test.ln882x-ard.yaml b/tests/components/script/test.ln882x-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/script/test.ln882x-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/sntp/test.ln882x-ard.yaml b/tests/components/sntp/test.ln882x-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/sntp/test.ln882x-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/switch/test.ln882x-ard.yaml b/tests/components/switch/test.ln882x-ard.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/switch/test.ln882x-ard.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/syslog/test.ln882x-ard.yaml b/tests/components/syslog/test.ln882x-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/syslog/test.ln882x-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/template/test.ln882x-ard.yaml b/tests/components/template/test.ln882x-ard.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/template/test.ln882x-ard.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/test_build_components/build_components_base.ln882x-ard.yaml b/tests/test_build_components/build_components_base.ln882x-ard.yaml new file mode 100644 index 0000000000..80fc6690f9 --- /dev/null +++ b/tests/test_build_components/build_components_base.ln882x-ard.yaml @@ -0,0 +1,15 @@ +esphome: + name: componenttestespln882x + friendly_name: $component_name + +ln882x: + board: generic-ln882hki + +logger: + level: VERY_VERBOSE + +packages: + component_under_test: !include + file: $component_test_file + vars: + component_test_file: $component_test_file diff --git a/tests/unit_tests/test_config_validation.py b/tests/unit_tests/test_config_validation.py index 7a1354589c..2928c5c83a 100644 --- a/tests/unit_tests/test_config_validation.py +++ b/tests/unit_tests/test_config_validation.py @@ -20,6 +20,7 @@ from esphome.const import ( PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_HOST, + PLATFORM_LN882X, PLATFORM_RP2040, PLATFORM_RTL87XX, ) @@ -214,7 +215,8 @@ def hex_int__valid(value): ("arduino", PLATFORM_RP2040, None, "20", "20", "20", "20"), ("arduino", PLATFORM_BK72XX, None, "21", "21", "21", "21"), ("arduino", PLATFORM_RTL87XX, None, "22", "22", "22", "22"), - ("host", PLATFORM_HOST, None, "23", "23", "23", "23"), + ("arduino", PLATFORM_LN882X, None, "23", "23", "23", "23"), + ("host", PLATFORM_HOST, None, "24", "24", "24", "24"), ], ) def test_split_default(framework, platform, variant, full, idf, arduino, simple): @@ -244,7 +246,8 @@ def test_split_default(framework, platform, variant, full, idf, arduino, simple) "rp2040": "20", "bk72xx": "21", "rtl87xx": "22", - "host": "23", + "ln882x": "23", + "host": "24", } idf_mappings = { diff --git a/tests/unit_tests/test_wizard.py b/tests/unit_tests/test_wizard.py index 6d360740f4..ab20b2abb5 100644 --- a/tests/unit_tests/test_wizard.py +++ b/tests/unit_tests/test_wizard.py @@ -8,6 +8,7 @@ import pytest from esphome.components.bk72xx.boards import BK72XX_BOARD_PINS from esphome.components.esp32.boards import ESP32_BOARD_PINS from esphome.components.esp8266.boards import ESP8266_BOARD_PINS +from esphome.components.ln882x.boards import LN882X_BOARD_PINS from esphome.components.rtl87xx.boards import RTL87XX_BOARD_PINS from esphome.core import CORE import esphome.wizard as wz @@ -187,6 +188,27 @@ def test_wizard_write_defaults_platform_from_board_bk72xx( assert "bk72xx:" in generated_config +def test_wizard_write_defaults_platform_from_board_ln882x( + default_config, tmp_path, monkeypatch +): + """ + If the platform is not explicitly set, use "LN882X" if the board is one of LN882X boards + """ + # Given + del default_config["platform"] + default_config["board"] = [*LN882X_BOARD_PINS][0] + + monkeypatch.setattr(wz, "write_file", MagicMock()) + monkeypatch.setattr(CORE, "config_path", os.path.dirname(tmp_path)) + + # When + wz.wizard_write(tmp_path, **default_config) + + # Then + generated_config = wz.write_file.call_args.args[1] + assert "ln882x:" in generated_config + + def test_wizard_write_defaults_platform_from_board_rtl87xx( default_config, tmp_path, monkeypatch ): From 8a9769d4e958e6f9773ed245fa1aa11ea8ca1f37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Mork?= Date: Mon, 30 Jun 2025 06:49:38 +0200 Subject: [PATCH 3/5] Support DM9051 SPI ethernet device (#6861) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/ethernet/__init__.py | 10 +++-- .../ethernet/ethernet_component.cpp | 41 +++++++++++++++++-- .../components/ethernet/ethernet_component.h | 1 + tests/components/ethernet/common-dm9051.yaml | 14 +++++++ .../ethernet/test-dm9051.esp32-ard.yaml | 1 + .../ethernet/test-dm9051.esp32-idf.yaml | 1 + 6 files changed, 62 insertions(+), 6 deletions(-) create mode 100644 tests/components/ethernet/common-dm9051.yaml create mode 100644 tests/components/ethernet/test-dm9051.esp32-ard.yaml create mode 100644 tests/components/ethernet/test-dm9051.esp32-idf.yaml diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index cd77ea6053..8eec9510cc 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -66,9 +66,10 @@ ETHERNET_TYPES = { "KSZ8081RNA": EthernetType.ETHERNET_TYPE_KSZ8081RNA, "W5500": EthernetType.ETHERNET_TYPE_W5500, "OPENETH": EthernetType.ETHERNET_TYPE_OPENETH, + "DM9051": EthernetType.ETHERNET_TYPE_DM9051, } -SPI_ETHERNET_TYPES = ["W5500"] +SPI_ETHERNET_TYPES = ["W5500", "DM9051"] SPI_ETHERNET_DEFAULT_POLLING_INTERVAL = TimePeriodMilliseconds(milliseconds=10) emac_rmii_clock_mode_t = cg.global_ns.enum("emac_rmii_clock_mode_t") @@ -224,6 +225,7 @@ CONFIG_SCHEMA = cv.All( "KSZ8081RNA": RMII_SCHEMA, "W5500": SPI_SCHEMA, "OPENETH": BASE_SCHEMA, + "DM9051": SPI_SCHEMA, }, upper=True, ), @@ -278,7 +280,7 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - if config[CONF_TYPE] == "W5500": + if config[CONF_TYPE] in SPI_ETHERNET_TYPES: cg.add(var.set_clk_pin(config[CONF_CLK_PIN])) cg.add(var.set_miso_pin(config[CONF_MISO_PIN])) cg.add(var.set_mosi_pin(config[CONF_MOSI_PIN])) @@ -296,7 +298,9 @@ async def to_code(config): cg.add_define("USE_ETHERNET_SPI") if CORE.using_esp_idf: add_idf_sdkconfig_option("CONFIG_ETH_USE_SPI_ETHERNET", True) - add_idf_sdkconfig_option("CONFIG_ETH_SPI_ETHERNET_W5500", True) + add_idf_sdkconfig_option( + f"CONFIG_ETH_SPI_ETHERNET_{config[CONF_TYPE]}", True + ) elif config[CONF_TYPE] == "OPENETH": cg.add_define("USE_ETHERNET_OPENETH") add_idf_sdkconfig_option("CONFIG_ETH_USE_OPENETH", True) diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 180a72ec7e..8739269f4a 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -90,8 +90,8 @@ void EthernetComponent::setup() { #ifdef USE_ETHERNET_SPI // Configure SPI interface and Ethernet driver for specific SPI module spi_device_interface_config_t devcfg = { - .command_bits = 16, // Actually it's the address phase in W5500 SPI frame - .address_bits = 8, // Actually it's the control phase in W5500 SPI frame + .command_bits = 0, + .address_bits = 0, .dummy_bits = 0, .mode = 0, .duty_cycle_pos = 0, @@ -107,22 +107,43 @@ void EthernetComponent::setup() { }; #if ESP_IDF_VERSION_MAJOR >= 5 +#if CONFIG_ETH_SPI_ETHERNET_W5500 eth_w5500_config_t w5500_config = ETH_W5500_DEFAULT_CONFIG(host, &devcfg); +#endif +#if CONFIG_ETH_SPI_ETHERNET_DM9051 + eth_dm9051_config_t dm9051_config = ETH_DM9051_DEFAULT_CONFIG(host, &devcfg); +#endif #else spi_device_handle_t spi_handle = nullptr; err = spi_bus_add_device(host, &devcfg, &spi_handle); ESPHL_ERROR_CHECK(err, "SPI bus add device error"); +#if CONFIG_ETH_SPI_ETHERNET_W5500 eth_w5500_config_t w5500_config = ETH_W5500_DEFAULT_CONFIG(spi_handle); #endif +#if CONFIG_ETH_SPI_ETHERNET_DM9051 + eth_dm9051_config_t dm9051_config = ETH_DM9051_DEFAULT_CONFIG(spi_handle); +#endif +#endif // ESP_IDF_VERSION_MAJOR >= 5 + +#if CONFIG_ETH_SPI_ETHERNET_W5500 w5500_config.int_gpio_num = this->interrupt_pin_; #ifdef USE_ETHERNET_SPI_POLLING_SUPPORT w5500_config.poll_period_ms = this->polling_interval_; #endif +#endif + +#if CONFIG_ETH_SPI_ETHERNET_DM9051 + dm9051_config.int_gpio_num = this->interrupt_pin_; +#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT + dm9051_config.poll_period_ms = this->polling_interval_; +#endif +#endif + phy_config.phy_addr = this->phy_addr_spi_; phy_config.reset_gpio_num = this->reset_pin_; - esp_eth_mac_t *mac = esp_eth_mac_new_w5500(&w5500_config, &mac_config); + esp_eth_mac_t *mac = nullptr; #elif defined(USE_ETHERNET_OPENETH) esp_eth_mac_t *mac = esp_eth_mac_new_openeth(&mac_config); #else @@ -187,10 +208,20 @@ void EthernetComponent::setup() { } #endif #ifdef USE_ETHERNET_SPI +#if CONFIG_ETH_SPI_ETHERNET_W5500 case ETHERNET_TYPE_W5500: { + mac = esp_eth_mac_new_w5500(&w5500_config, &mac_config); this->phy_ = esp_eth_phy_new_w5500(&phy_config); break; } +#endif +#if CONFIG_ETH_SPI_ETHERNET_DM9051 + case ETHERNET_TYPE_DM9051: { + mac = esp_eth_mac_new_dm9051(&dm9051_config, &mac_config); + this->phy_ = esp_eth_phy_new_dm9051(&phy_config); + break; + } +#endif #endif default: { this->mark_failed(); @@ -321,6 +352,10 @@ void EthernetComponent::dump_config() { eth_type = "OPENETH"; break; + case ETHERNET_TYPE_DM9051: + eth_type = "DM9051"; + break; + default: eth_type = "Unknown"; break; diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index 0f0eff5ded..6cdc113aa8 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -26,6 +26,7 @@ enum EthernetType : uint8_t { ETHERNET_TYPE_KSZ8081RNA, ETHERNET_TYPE_W5500, ETHERNET_TYPE_OPENETH, + ETHERNET_TYPE_DM9051, }; struct ManualIP { diff --git a/tests/components/ethernet/common-dm9051.yaml b/tests/components/ethernet/common-dm9051.yaml new file mode 100644 index 0000000000..c878ca6e59 --- /dev/null +++ b/tests/components/ethernet/common-dm9051.yaml @@ -0,0 +1,14 @@ +ethernet: + type: DM9051 + clk_pin: 19 + mosi_pin: 21 + miso_pin: 23 + cs_pin: 18 + interrupt_pin: 36 + reset_pin: 22 + clock_speed: 10Mhz + manual_ip: + static_ip: 192.168.178.56 + gateway: 192.168.178.1 + subnet: 255.255.255.0 + domain: .local diff --git a/tests/components/ethernet/test-dm9051.esp32-ard.yaml b/tests/components/ethernet/test-dm9051.esp32-ard.yaml new file mode 100644 index 0000000000..23e3b97740 --- /dev/null +++ b/tests/components/ethernet/test-dm9051.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common-dm9051.yaml diff --git a/tests/components/ethernet/test-dm9051.esp32-idf.yaml b/tests/components/ethernet/test-dm9051.esp32-idf.yaml new file mode 100644 index 0000000000..23e3b97740 --- /dev/null +++ b/tests/components/ethernet/test-dm9051.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common-dm9051.yaml From af0bb634c65c4b02110ad753f7b801437b1af6e7 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Mon, 30 Jun 2025 00:05:52 -0500 Subject: [PATCH 4/5] [light] Fix transitions with ``lerp`` (#9269) --- esphome/components/light/light_color_values.h | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/esphome/components/light/light_color_values.h b/esphome/components/light/light_color_values.h index ca32b9c571..d8eaa6ae24 100644 --- a/esphome/components/light/light_color_values.h +++ b/esphome/components/light/light_color_values.h @@ -86,16 +86,16 @@ class LightColorValues { static LightColorValues lerp(const LightColorValues &start, const LightColorValues &end, float completion) { LightColorValues v; v.set_color_mode(end.color_mode_); - v.set_state(esphome::lerp(completion, start.get_state(), end.get_state())); - v.set_brightness(esphome::lerp(completion, start.get_brightness(), end.get_brightness())); - v.set_color_brightness(esphome::lerp(completion, start.get_color_brightness(), end.get_color_brightness())); - v.set_red(esphome::lerp(completion, start.get_red(), end.get_red())); - v.set_green(esphome::lerp(completion, start.get_green(), end.get_green())); - v.set_blue(esphome::lerp(completion, start.get_blue(), end.get_blue())); - v.set_white(esphome::lerp(completion, start.get_white(), end.get_white())); - v.set_color_temperature(esphome::lerp(completion, start.get_color_temperature(), end.get_color_temperature())); - v.set_cold_white(esphome::lerp(completion, start.get_cold_white(), end.get_cold_white())); - v.set_warm_white(esphome::lerp(completion, start.get_warm_white(), end.get_warm_white())); + v.set_state(std::lerp(start.get_state(), end.get_state(), completion)); + v.set_brightness(std::lerp(start.get_brightness(), end.get_brightness(), completion)); + v.set_color_brightness(std::lerp(start.get_color_brightness(), end.get_color_brightness(), completion)); + v.set_red(std::lerp(start.get_red(), end.get_red(), completion)); + v.set_green(std::lerp(start.get_green(), end.get_green(), completion)); + v.set_blue(std::lerp(start.get_blue(), end.get_blue(), completion)); + v.set_white(std::lerp(start.get_white(), end.get_white(), completion)); + v.set_color_temperature(std::lerp(start.get_color_temperature(), end.get_color_temperature(), completion)); + v.set_cold_white(std::lerp(start.get_cold_white(), end.get_cold_white(), completion)); + v.set_warm_white(std::lerp(start.get_warm_white(), end.get_warm_white(), completion)); return v; } From 3e553f517b2f5683182d0a447db22130b5d6fc69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Poczkodi?= Date: Mon, 30 Jun 2025 07:12:44 +0200 Subject: [PATCH 5/5] [remote_base] Fix dumper base class and enable schema extension (#9218) --- esphome/components/remote_base/__init__.py | 12 ++++++---- .../components/remote_base/remote_base.cpp | 24 +++++++++++++++++++ esphome/components/remote_base/remote_base.h | 3 +++ 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 836b98104b..fc824ef704 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -57,7 +57,7 @@ RemoteReceiverBinarySensorBase = ns.class_( RemoteReceiverTrigger = ns.class_( "RemoteReceiverTrigger", automation.Trigger, RemoteReceiverListener ) -RemoteTransmitterDumper = ns.class_("RemoteTransmitterDumper") +RemoteReceiverDumperBase = ns.class_("RemoteReceiverDumperBase") RemoteTransmittable = ns.class_("RemoteTransmittable") RemoteTransmitterActionBase = ns.class_( "RemoteTransmitterActionBase", RemoteTransmittable, automation.Action @@ -126,8 +126,10 @@ def register_trigger(name, type, data_type): return decorator -def register_dumper(name, type): - registerer = DUMPER_REGISTRY.register(name, type, {}) +def register_dumper(name, type, schema=None): + if schema is None: + schema = {} + registerer = DUMPER_REGISTRY.register(name, type, schema) def decorator(func): async def new_func(config, dumper_id): @@ -189,7 +191,7 @@ def declare_protocol(name): binary_sensor_ = ns.class_(f"{name}BinarySensor", RemoteReceiverBinarySensorBase) trigger = ns.class_(f"{name}Trigger", RemoteReceiverTrigger) action = ns.class_(f"{name}Action", RemoteTransmitterActionBase) - dumper = ns.class_(f"{name}Dumper", RemoteTransmitterDumper) + dumper = ns.class_(f"{name}Dumper", RemoteReceiverDumperBase) return data, binary_sensor_, trigger, action, dumper @@ -1405,7 +1407,7 @@ rc_switch_protocols = ns.RC_SWITCH_PROTOCOLS RCSwitchData = ns.struct("RCSwitchData") RCSwitchBase = ns.class_("RCSwitchBase") RCSwitchTrigger = ns.class_("RCSwitchTrigger", RemoteReceiverTrigger) -RCSwitchDumper = ns.class_("RCSwitchDumper", RemoteTransmitterDumper) +RCSwitchDumper = ns.class_("RCSwitchDumper", RemoteReceiverDumperBase) RCSwitchRawAction = ns.class_("RCSwitchRawAction", RemoteTransmitterActionBase) RCSwitchTypeAAction = ns.class_("RCSwitchTypeAAction", RemoteTransmitterActionBase) RCSwitchTypeBAction = ns.class_("RCSwitchTypeBAction", RemoteTransmitterActionBase) diff --git a/esphome/components/remote_base/remote_base.cpp b/esphome/components/remote_base/remote_base.cpp index 987286b345..34aba236b3 100644 --- a/esphome/components/remote_base/remote_base.cpp +++ b/esphome/components/remote_base/remote_base.cpp @@ -19,6 +19,22 @@ bool RemoteReceiveData::peek_mark(uint32_t length, uint32_t offset) const { return value >= 0 && lo <= value && value <= hi; } +bool RemoteReceiveData::peek_mark_at_least(uint32_t length, uint32_t offset) const { + if (!this->is_valid(offset)) + return false; + const int32_t value = this->peek(offset); + const int32_t lo = this->lower_bound_(length); + return value >= 0 && lo <= value; +} + +bool RemoteReceiveData::peek_mark_at_most(uint32_t length, uint32_t offset) const { + if (!this->is_valid(offset)) + return false; + const int32_t value = this->peek(offset); + const int32_t hi = this->upper_bound_(length); + return value >= 0 && value <= hi; +} + bool RemoteReceiveData::peek_space(uint32_t length, uint32_t offset) const { if (!this->is_valid(offset)) return false; @@ -36,6 +52,14 @@ bool RemoteReceiveData::peek_space_at_least(uint32_t length, uint32_t offset) co return value <= 0 && lo <= -value; } +bool RemoteReceiveData::peek_space_at_most(uint32_t length, uint32_t offset) const { + if (!this->is_valid(offset)) + return false; + const int32_t value = this->peek(offset); + const int32_t hi = this->upper_bound_(length); + return value <= 0 && -value <= hi; +} + bool RemoteReceiveData::expect_mark(uint32_t length) { if (!this->peek_mark(length)) return false; diff --git a/esphome/components/remote_base/remote_base.h b/esphome/components/remote_base/remote_base.h index a18dd0ed7e..b740ba8085 100644 --- a/esphome/components/remote_base/remote_base.h +++ b/esphome/components/remote_base/remote_base.h @@ -53,8 +53,11 @@ class RemoteReceiveData { bool is_valid(uint32_t offset = 0) const { return this->index_ + offset < this->data_.size(); } int32_t peek(uint32_t offset = 0) const { return this->data_[this->index_ + offset]; } bool peek_mark(uint32_t length, uint32_t offset = 0) const; + bool peek_mark_at_least(uint32_t length, uint32_t offset = 0) const; + bool peek_mark_at_most(uint32_t length, uint32_t offset = 0) const; bool peek_space(uint32_t length, uint32_t offset = 0) const; bool peek_space_at_least(uint32_t length, uint32_t offset = 0) const; + bool peek_space_at_most(uint32_t length, uint32_t offset = 0) const; bool peek_item(uint32_t mark, uint32_t space, uint32_t offset = 0) const { return this->peek_space(space, offset + 1) && this->peek_mark(mark, offset); }