diff --git a/esphome/components/web_server/list_entities.cpp b/esphome/components/web_server/list_entities.cpp index a02f84c34b..fb02821760 100644 --- a/esphome/components/web_server/list_entities.cpp +++ b/esphome/components/web_server/list_entities.cpp @@ -9,180 +9,184 @@ namespace esphome { namespace web_server { -ListEntitiesIterator::ListEntitiesIterator(WebServer *web_server) : web_server_(web_server) {} +#ifdef USE_ARDUINO +ListEntitiesIterator::ListEntitiesIterator(const WebServer *ws, DeferredUpdateEventSource *es) + : web_server_(ws), events_(es) {} +#endif +#ifdef USE_ESP_IDF +ListEntitiesIterator::ListEntitiesIterator(const WebServer *ws, AsyncEventSource *es) : web_server_(ws), events_(es) {} +#endif +ListEntitiesIterator::~ListEntitiesIterator() {} #ifdef USE_BINARY_SENSOR -bool ListEntitiesIterator::on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) { - if (this->web_server_->events_.count() == 0) +bool ListEntitiesIterator::on_binary_sensor(binary_sensor::BinarySensor *obj) { + if (this->events_->count() == 0) return true; - this->web_server_->events_.send( - this->web_server_->binary_sensor_json(binary_sensor, binary_sensor->state, DETAIL_ALL).c_str(), "state"); + this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::binary_sensor_all_json_generator); return true; } #endif #ifdef USE_COVER -bool ListEntitiesIterator::on_cover(cover::Cover *cover) { - if (this->web_server_->events_.count() == 0) +bool ListEntitiesIterator::on_cover(cover::Cover *obj) { + if (this->events_->count() == 0) return true; - this->web_server_->events_.send(this->web_server_->cover_json(cover, DETAIL_ALL).c_str(), "state"); + this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::cover_all_json_generator); return true; } #endif #ifdef USE_FAN -bool ListEntitiesIterator::on_fan(fan::Fan *fan) { - if (this->web_server_->events_.count() == 0) +bool ListEntitiesIterator::on_fan(fan::Fan *obj) { + if (this->events_->count() == 0) return true; - this->web_server_->events_.send(this->web_server_->fan_json(fan, DETAIL_ALL).c_str(), "state"); + this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::fan_all_json_generator); return true; } #endif #ifdef USE_LIGHT -bool ListEntitiesIterator::on_light(light::LightState *light) { - if (this->web_server_->events_.count() == 0) +bool ListEntitiesIterator::on_light(light::LightState *obj) { + if (this->events_->count() == 0) return true; - this->web_server_->events_.send(this->web_server_->light_json(light, DETAIL_ALL).c_str(), "state"); + this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::light_all_json_generator); return true; } #endif #ifdef USE_SENSOR -bool ListEntitiesIterator::on_sensor(sensor::Sensor *sensor) { - if (this->web_server_->events_.count() == 0) +bool ListEntitiesIterator::on_sensor(sensor::Sensor *obj) { + if (this->events_->count() == 0) return true; - this->web_server_->events_.send(this->web_server_->sensor_json(sensor, sensor->state, DETAIL_ALL).c_str(), "state"); + this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::sensor_all_json_generator); return true; } #endif #ifdef USE_SWITCH -bool ListEntitiesIterator::on_switch(switch_::Switch *a_switch) { - if (this->web_server_->events_.count() == 0) +bool ListEntitiesIterator::on_switch(switch_::Switch *obj) { + if (this->events_->count() == 0) return true; - this->web_server_->events_.send(this->web_server_->switch_json(a_switch, a_switch->state, DETAIL_ALL).c_str(), - "state"); + this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::switch_all_json_generator); return true; } #endif #ifdef USE_BUTTON -bool ListEntitiesIterator::on_button(button::Button *button) { - if (this->web_server_->events_.count() == 0) +bool ListEntitiesIterator::on_button(button::Button *obj) { + if (this->events_->count() == 0) return true; - this->web_server_->events_.send(this->web_server_->button_json(button, DETAIL_ALL).c_str(), "state"); + this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::button_all_json_generator); return true; } #endif #ifdef USE_TEXT_SENSOR -bool ListEntitiesIterator::on_text_sensor(text_sensor::TextSensor *text_sensor) { - if (this->web_server_->events_.count() == 0) +bool ListEntitiesIterator::on_text_sensor(text_sensor::TextSensor *obj) { + if (this->events_->count() == 0) return true; - this->web_server_->events_.send( - this->web_server_->text_sensor_json(text_sensor, text_sensor->state, DETAIL_ALL).c_str(), "state"); + this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::text_sensor_all_json_generator); return true; } #endif #ifdef USE_LOCK -bool ListEntitiesIterator::on_lock(lock::Lock *a_lock) { - if (this->web_server_->events_.count() == 0) +bool ListEntitiesIterator::on_lock(lock::Lock *obj) { + if (this->events_->count() == 0) return true; - this->web_server_->events_.send(this->web_server_->lock_json(a_lock, a_lock->state, DETAIL_ALL).c_str(), "state"); + this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::lock_all_json_generator); return true; } #endif #ifdef USE_VALVE -bool ListEntitiesIterator::on_valve(valve::Valve *valve) { - if (this->web_server_->events_.count() == 0) +bool ListEntitiesIterator::on_valve(valve::Valve *obj) { + if (this->events_->count() == 0) return true; - this->web_server_->events_.send(this->web_server_->valve_json(valve, DETAIL_ALL).c_str(), "state"); + this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::valve_all_json_generator); return true; } #endif #ifdef USE_CLIMATE -bool ListEntitiesIterator::on_climate(climate::Climate *climate) { - if (this->web_server_->events_.count() == 0) +bool ListEntitiesIterator::on_climate(climate::Climate *obj) { + if (this->events_->count() == 0) return true; - this->web_server_->events_.send(this->web_server_->climate_json(climate, DETAIL_ALL).c_str(), "state"); + this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::climate_all_json_generator); return true; } #endif #ifdef USE_NUMBER -bool ListEntitiesIterator::on_number(number::Number *number) { - if (this->web_server_->events_.count() == 0) +bool ListEntitiesIterator::on_number(number::Number *obj) { + if (this->events_->count() == 0) return true; - this->web_server_->events_.send(this->web_server_->number_json(number, number->state, DETAIL_ALL).c_str(), "state"); + this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::number_all_json_generator); return true; } #endif #ifdef USE_DATETIME_DATE -bool ListEntitiesIterator::on_date(datetime::DateEntity *date) { - if (this->web_server_->events_.count() == 0) +bool ListEntitiesIterator::on_date(datetime::DateEntity *obj) { + if (this->events_->count() == 0) return true; - this->web_server_->events_.send(this->web_server_->date_json(date, DETAIL_ALL).c_str(), "state"); + this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::date_all_json_generator); return true; } #endif #ifdef USE_DATETIME_TIME -bool ListEntitiesIterator::on_time(datetime::TimeEntity *time) { - this->web_server_->events_.send(this->web_server_->time_json(time, DETAIL_ALL).c_str(), "state"); +bool ListEntitiesIterator::on_time(datetime::TimeEntity *obj) { + if (this->events_->count() == 0) + return true; + this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::time_all_json_generator); return true; } #endif #ifdef USE_DATETIME_DATETIME -bool ListEntitiesIterator::on_datetime(datetime::DateTimeEntity *datetime) { - if (this->web_server_->events_.count() == 0) +bool ListEntitiesIterator::on_datetime(datetime::DateTimeEntity *obj) { + if (this->events_->count() == 0) return true; - this->web_server_->events_.send(this->web_server_->datetime_json(datetime, DETAIL_ALL).c_str(), "state"); + this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::datetime_all_json_generator); return true; } #endif #ifdef USE_TEXT -bool ListEntitiesIterator::on_text(text::Text *text) { - if (this->web_server_->events_.count() == 0) +bool ListEntitiesIterator::on_text(text::Text *obj) { + if (this->events_->count() == 0) return true; - this->web_server_->events_.send(this->web_server_->text_json(text, text->state, DETAIL_ALL).c_str(), "state"); + this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::text_all_json_generator); return true; } #endif #ifdef USE_SELECT -bool ListEntitiesIterator::on_select(select::Select *select) { - if (this->web_server_->events_.count() == 0) +bool ListEntitiesIterator::on_select(select::Select *obj) { + if (this->events_->count() == 0) return true; - this->web_server_->events_.send(this->web_server_->select_json(select, select->state, DETAIL_ALL).c_str(), "state"); + this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::select_all_json_generator); return true; } #endif #ifdef USE_ALARM_CONTROL_PANEL -bool ListEntitiesIterator::on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { - if (this->web_server_->events_.count() == 0) +bool ListEntitiesIterator::on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *obj) { + if (this->events_->count() == 0) return true; - this->web_server_->events_.send( - this->web_server_->alarm_control_panel_json(a_alarm_control_panel, a_alarm_control_panel->get_state(), DETAIL_ALL) - .c_str(), - "state"); + this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::alarm_control_panel_all_json_generator); return true; } #endif #ifdef USE_EVENT -bool ListEntitiesIterator::on_event(event::Event *event) { +bool ListEntitiesIterator::on_event(event::Event *obj) { + if (this->events_->count() == 0) + return true; // Null event type, since we are just iterating over entities - const std::string null_event_type = ""; - this->web_server_->events_.send(this->web_server_->event_json(event, null_event_type, DETAIL_ALL).c_str(), "state"); + this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::event_all_json_generator); return true; } #endif #ifdef USE_UPDATE -bool ListEntitiesIterator::on_update(update::UpdateEntity *update) { - if (this->web_server_->events_.count() == 0) +bool ListEntitiesIterator::on_update(update::UpdateEntity *obj) { + if (this->events_->count() == 0) return true; - this->web_server_->events_.send(this->web_server_->update_json(update, DETAIL_ALL).c_str(), "state"); + this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::update_all_json_generator); return true; } #endif diff --git a/esphome/components/web_server/list_entities.h b/esphome/components/web_server/list_entities.h index 53e5bc3355..ba81c70c86 100644 --- a/esphome/components/web_server/list_entities.h +++ b/esphome/components/web_server/list_entities.h @@ -5,76 +5,97 @@ #include "esphome/core/component.h" #include "esphome/core/component_iterator.h" namespace esphome { +#ifdef USE_ESP_IDF +namespace web_server_idf { +class AsyncEventSource; +} +#endif namespace web_server { +#ifdef USE_ARDUINO +class DeferredUpdateEventSource; +#endif class WebServer; class ListEntitiesIterator : public ComponentIterator { public: - ListEntitiesIterator(WebServer *web_server); +#ifdef USE_ARDUINO + ListEntitiesIterator(const WebServer *ws, DeferredUpdateEventSource *es); +#endif +#ifdef USE_ESP_IDF + ListEntitiesIterator(const WebServer *ws, esphome::web_server_idf::AsyncEventSource *es); +#endif + virtual ~ListEntitiesIterator(); #ifdef USE_BINARY_SENSOR - bool on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) override; + bool on_binary_sensor(binary_sensor::BinarySensor *obj) override; #endif #ifdef USE_COVER - bool on_cover(cover::Cover *cover) override; + bool on_cover(cover::Cover *obj) override; #endif #ifdef USE_FAN - bool on_fan(fan::Fan *fan) override; + bool on_fan(fan::Fan *obj) override; #endif #ifdef USE_LIGHT - bool on_light(light::LightState *light) override; + bool on_light(light::LightState *obj) override; #endif #ifdef USE_SENSOR - bool on_sensor(sensor::Sensor *sensor) override; + bool on_sensor(sensor::Sensor *obj) override; #endif #ifdef USE_SWITCH - bool on_switch(switch_::Switch *a_switch) override; + bool on_switch(switch_::Switch *obj) override; #endif #ifdef USE_BUTTON - bool on_button(button::Button *button) override; + bool on_button(button::Button *obj) override; #endif #ifdef USE_TEXT_SENSOR - bool on_text_sensor(text_sensor::TextSensor *text_sensor) override; + bool on_text_sensor(text_sensor::TextSensor *obj) override; #endif #ifdef USE_CLIMATE - bool on_climate(climate::Climate *climate) override; + bool on_climate(climate::Climate *obj) override; #endif #ifdef USE_NUMBER - bool on_number(number::Number *number) override; + bool on_number(number::Number *obj) override; #endif #ifdef USE_DATETIME_DATE - bool on_date(datetime::DateEntity *date) override; + bool on_date(datetime::DateEntity *obj) override; #endif #ifdef USE_DATETIME_TIME - bool on_time(datetime::TimeEntity *time) override; + bool on_time(datetime::TimeEntity *obj) override; #endif #ifdef USE_DATETIME_DATETIME - bool on_datetime(datetime::DateTimeEntity *datetime) override; + bool on_datetime(datetime::DateTimeEntity *obj) override; #endif #ifdef USE_TEXT - bool on_text(text::Text *text) override; + bool on_text(text::Text *obj) override; #endif #ifdef USE_SELECT - bool on_select(select::Select *select) override; + bool on_select(select::Select *obj) override; #endif #ifdef USE_LOCK - bool on_lock(lock::Lock *a_lock) override; + bool on_lock(lock::Lock *obj) override; #endif #ifdef USE_VALVE - bool on_valve(valve::Valve *valve) override; + bool on_valve(valve::Valve *obj) override; #endif #ifdef USE_ALARM_CONTROL_PANEL - bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) override; + bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *obj) override; #endif #ifdef USE_EVENT - bool on_event(event::Event *event) override; + bool on_event(event::Event *obj) override; #endif #ifdef USE_UPDATE - bool on_update(update::UpdateEntity *update) override; + bool on_update(update::UpdateEntity *obj) override; #endif + bool completed() { return this->state_ == IteratorState::NONE; } protected: - WebServer *web_server_; + const WebServer *web_server_; +#ifdef USE_ARDUINO + DeferredUpdateEventSource *events_; +#endif +#ifdef USE_ESP_IDF + esphome::web_server_idf::AsyncEventSource *events_; +#endif }; } // namespace web_server diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 8c09d607a7..63c1d5d4fd 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -72,8 +72,146 @@ UrlMatch match_url(const std::string &url, bool only_domain = false) { return match; } -WebServer::WebServer(web_server_base::WebServerBase *base) - : base_(base), entities_iterator_(ListEntitiesIterator(this)) { +#ifdef USE_ARDUINO +// helper for allowing only unique entries in the queue +void DeferredUpdateEventSource::deq_push_back_with_dedup_(void *source, message_generator_t *message_generator) { + DeferredEvent item(source, message_generator); + + auto iter = std::find_if(this->deferred_queue_.begin(), this->deferred_queue_.end(), + [&item](const DeferredEvent &test) -> bool { return test == item; }); + + if (iter != this->deferred_queue_.end()) { + (*iter) = item; + } else { + this->deferred_queue_.push_back(item); + } +} + +void DeferredUpdateEventSource::process_deferred_queue_() { + while (!deferred_queue_.empty()) { + DeferredEvent &de = deferred_queue_.front(); + std::string message = de.message_generator_(web_server_, de.source_); + if (this->try_send(message.c_str(), "state")) { + // O(n) but memory efficiency is more important than speed here which is why std::vector was chosen + deferred_queue_.erase(deferred_queue_.begin()); + } else { + break; + } + } +} + +void DeferredUpdateEventSource::loop() { + process_deferred_queue_(); + if (!this->entities_iterator_.completed()) + this->entities_iterator_.advance(); +} + +void DeferredUpdateEventSource::deferrable_send_state(void *source, const char *event_type, + message_generator_t *message_generator) { + // allow all json "details_all" to go through before publishing bare state events, this avoids unnamed entries showing + // up in the web GUI and reduces event load during initial connect + if (!entities_iterator_.completed() && 0 != strcmp(event_type, "state_detail_all")) + return; + + if (source == nullptr) + return; + if (event_type == nullptr) + return; + if (message_generator == nullptr) + return; + + if (0 != strcmp(event_type, "state_detail_all") && 0 != strcmp(event_type, "state")) { + ESP_LOGE(TAG, "Can't defer non-state event"); + } + + if (!deferred_queue_.empty()) + process_deferred_queue_(); + if (!deferred_queue_.empty()) { + // deferred queue still not empty which means downstream event queue full, no point trying to send first + deq_push_back_with_dedup_(source, message_generator); + } else { + std::string message = message_generator(web_server_, source); + if (!this->try_send(message.c_str(), "state")) { + deq_push_back_with_dedup_(source, message_generator); + } + } +} + +// used for logs plus the initial ping/config +void DeferredUpdateEventSource::try_send_nodefer(const char *message, const char *event, uint32_t id, + uint32_t reconnect) { + this->send(message, event, id, reconnect); +} + +void DeferredUpdateEventSourceList::loop() { + for (DeferredUpdateEventSource *dues : *this) { + dues->loop(); + } +} + +void DeferredUpdateEventSourceList::deferrable_send_state(void *source, const char *event_type, + message_generator_t *message_generator) { + for (DeferredUpdateEventSource *dues : *this) { + dues->deferrable_send_state(source, event_type, message_generator); + } +} + +void DeferredUpdateEventSourceList::try_send_nodefer(const char *message, const char *event, uint32_t id, + uint32_t reconnect) { + for (DeferredUpdateEventSource *dues : *this) { + dues->try_send_nodefer(message, event, id, reconnect); + } +} + +void DeferredUpdateEventSourceList::add_new_client(WebServer *ws, AsyncWebServerRequest *request) { + DeferredUpdateEventSource *es = new DeferredUpdateEventSource(ws, "/events"); + this->push_back(es); + + es->onConnect([this, ws, es](AsyncEventSourceClient *client) { + ws->defer([this, ws, es]() { this->on_client_connect_(ws, es); }); + }); + + es->onDisconnect([this, ws](AsyncEventSource *source, AsyncEventSourceClient *client) { + ws->defer([this, source]() { this->on_client_disconnect_((DeferredUpdateEventSource *) source); }); + }); + + es->handleRequest(request); +} + +void DeferredUpdateEventSourceList::on_client_connect_(WebServer *ws, DeferredUpdateEventSource *source) { + // Configure reconnect timeout and send config + // this should always go through since the AsyncEventSourceClient event queue is empty on connect + std::string message = ws->get_config_json(); + source->try_send_nodefer(message.c_str(), "ping", millis(), 30000); + + for (auto &group : ws->sorting_groups_) { + message = json::build_json([group](JsonObject root) { + root["name"] = group.second.name; + root["sorting_weight"] = group.second.weight; + }); + + // up to 31 groups should be able to be queued initially without defer + source->try_send_nodefer(message.c_str(), "sorting_group"); + } + + source->entities_iterator_.begin(ws->include_internal_); + + // just dump them all up-front and take advantage of the deferred queue + // on second thought that takes too long, but leaving the commented code here for debug purposes + // while(!source->entities_iterator_.completed()) { + // source->entities_iterator_.advance(); + //} +} + +void DeferredUpdateEventSourceList::on_client_disconnect_(DeferredUpdateEventSource *source) { + // This method was called via WebServer->defer() and is no longer executing in the + // context of the network callback. The object is now dead and can be safely deleted. + this->remove(source); + delete source; // NOLINT +} +#endif + +WebServer::WebServer(web_server_base::WebServerBase *base) : base_(base) { #ifdef USE_ESP32 to_schedule_lock_ = xSemaphoreCreateMutex(); #endif @@ -101,34 +239,27 @@ void WebServer::setup() { this->setup_controller(this->include_internal_); this->base_->init(); - this->events_.onConnect([this](AsyncEventSourceClient *client) { - // Configure reconnect timeout and send config - client->send(this->get_config_json().c_str(), "ping", millis(), 30000); - - for (auto &group : this->sorting_groups_) { - client->send(json::build_json([group](JsonObject root) { - root["name"] = group.second.name; - root["sorting_weight"] = group.second.weight; - }).c_str(), - "sorting_group"); - } - - this->entities_iterator_.begin(this->include_internal_); - }); - #ifdef USE_LOGGER if (logger::global_logger != nullptr && this->expose_log_) { logger::global_logger->add_on_log_callback( - [this](int level, const char *tag, const char *message) { this->events_.send(message, "log", millis()); }); + // logs are not deferred, the memory overhead would be too large + [this](int level, const char *tag, const char *message) { + this->events_.try_send_nodefer(message, "log", millis()); + }); } #endif + +#ifdef USE_ESP_IDF this->base_->add_handler(&this->events_); +#endif this->base_->add_handler(this); if (this->allow_ota_) this->base_->add_ota_handler(); - this->set_interval(10000, [this]() { this->events_.send("", "ping", millis(), 30000); }); + // doesn't need defer functionality - if the queue is full, the client JS knows it's alive because it's clearly + // getting a lot of events + this->set_interval(10000, [this]() { this->events_.try_send_nodefer("", "ping", millis(), 30000); }); } void WebServer::loop() { #ifdef USE_ESP32 @@ -147,7 +278,8 @@ void WebServer::loop() { } } #endif - this->entities_iterator_.advance(); + + this->events_.loop(); } void WebServer::dump_config() { ESP_LOGCONFIG(TAG, "Web Server:"); @@ -219,9 +351,9 @@ void WebServer::handle_js_request(AsyncWebServerRequest *request) { #ifdef USE_SENSOR void WebServer::on_sensor_update(sensor::Sensor *obj, float state) { - if (this->events_.count() == 0) + if (this->events_.empty()) return; - this->events_.send(this->sensor_json(obj, state, DETAIL_STATE).c_str(), "state"); + this->events_.deferrable_send_state(obj, "state", sensor_state_json_generator); } void WebServer::handle_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (sensor::Sensor *obj : App.get_sensors()) { @@ -240,6 +372,12 @@ void WebServer::handle_sensor_request(AsyncWebServerRequest *request, const UrlM } request->send(404); } +std::string WebServer::sensor_state_json_generator(WebServer *web_server, void *source) { + return web_server->sensor_json((sensor::Sensor *) (source), ((sensor::Sensor *) (source))->state, DETAIL_STATE); +} +std::string WebServer::sensor_all_json_generator(WebServer *web_server, void *source) { + return web_server->sensor_json((sensor::Sensor *) (source), ((sensor::Sensor *) (source))->state, DETAIL_ALL); +} std::string WebServer::sensor_json(sensor::Sensor *obj, float value, JsonDetail start_config) { return json::build_json([this, obj, value, start_config](JsonObject root) { std::string state; @@ -267,9 +405,9 @@ std::string WebServer::sensor_json(sensor::Sensor *obj, float value, JsonDetail #ifdef USE_TEXT_SENSOR void WebServer::on_text_sensor_update(text_sensor::TextSensor *obj, const std::string &state) { - if (this->events_.count() == 0) + if (this->events_.empty()) return; - this->events_.send(this->text_sensor_json(obj, state, DETAIL_STATE).c_str(), "state"); + this->events_.deferrable_send_state(obj, "state", text_sensor_state_json_generator); } void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (text_sensor::TextSensor *obj : App.get_text_sensors()) { @@ -288,6 +426,14 @@ void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, const } request->send(404); } +std::string WebServer::text_sensor_state_json_generator(WebServer *web_server, void *source) { + return web_server->text_sensor_json((text_sensor::TextSensor *) (source), + ((text_sensor::TextSensor *) (source))->state, DETAIL_STATE); +} +std::string WebServer::text_sensor_all_json_generator(WebServer *web_server, void *source) { + return web_server->text_sensor_json((text_sensor::TextSensor *) (source), + ((text_sensor::TextSensor *) (source))->state, DETAIL_ALL); +} std::string WebServer::text_sensor_json(text_sensor::TextSensor *obj, const std::string &value, JsonDetail start_config) { return json::build_json([this, obj, value, start_config](JsonObject root) { @@ -306,9 +452,9 @@ std::string WebServer::text_sensor_json(text_sensor::TextSensor *obj, const std: #ifdef USE_SWITCH void WebServer::on_switch_update(switch_::Switch *obj, bool state) { - if (this->events_.count() == 0) + if (this->events_.empty()) return; - this->events_.send(this->switch_json(obj, state, DETAIL_STATE).c_str(), "state"); + this->events_.deferrable_send_state(obj, "state", switch_state_json_generator); } void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (switch_::Switch *obj : App.get_switches()) { @@ -339,6 +485,12 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM } request->send(404); } +std::string WebServer::switch_state_json_generator(WebServer *web_server, void *source) { + return web_server->switch_json((switch_::Switch *) (source), ((switch_::Switch *) (source))->state, DETAIL_STATE); +} +std::string WebServer::switch_all_json_generator(WebServer *web_server, void *source) { + return web_server->switch_json((switch_::Switch *) (source), ((switch_::Switch *) (source))->state, DETAIL_ALL); +} std::string WebServer::switch_json(switch_::Switch *obj, bool value, JsonDetail start_config) { return json::build_json([this, obj, value, start_config](JsonObject root) { set_json_icon_state_value(root, obj, "switch-" + obj->get_object_id(), value ? "ON" : "OFF", value, start_config); @@ -379,6 +531,12 @@ void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlM } request->send(404); } +std::string WebServer::button_state_json_generator(WebServer *web_server, void *source) { + return web_server->button_json((button::Button *) (source), DETAIL_STATE); +} +std::string WebServer::button_all_json_generator(WebServer *web_server, void *source) { + return web_server->button_json((button::Button *) (source), DETAIL_ALL); +} std::string WebServer::button_json(button::Button *obj, JsonDetail start_config) { return json::build_json([this, obj, start_config](JsonObject root) { set_json_id(root, obj, "button-" + obj->get_object_id(), start_config); @@ -396,9 +554,9 @@ std::string WebServer::button_json(button::Button *obj, JsonDetail start_config) #ifdef USE_BINARY_SENSOR void WebServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) { - if (this->events_.count() == 0) + if (this->events_.empty()) return; - this->events_.send(this->binary_sensor_json(obj, state, DETAIL_STATE).c_str(), "state"); + this->events_.deferrable_send_state(obj, "state", binary_sensor_state_json_generator); } void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (binary_sensor::BinarySensor *obj : App.get_binary_sensors()) { @@ -417,6 +575,14 @@ void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, con } request->send(404); } +std::string WebServer::binary_sensor_state_json_generator(WebServer *web_server, void *source) { + return web_server->binary_sensor_json((binary_sensor::BinarySensor *) (source), + ((binary_sensor::BinarySensor *) (source))->state, DETAIL_STATE); +} +std::string WebServer::binary_sensor_all_json_generator(WebServer *web_server, void *source) { + return web_server->binary_sensor_json((binary_sensor::BinarySensor *) (source), + ((binary_sensor::BinarySensor *) (source))->state, DETAIL_ALL); +} std::string WebServer::binary_sensor_json(binary_sensor::BinarySensor *obj, bool value, JsonDetail start_config) { return json::build_json([this, obj, value, start_config](JsonObject root) { set_json_icon_state_value(root, obj, "binary_sensor-" + obj->get_object_id(), value ? "ON" : "OFF", value, @@ -435,9 +601,9 @@ std::string WebServer::binary_sensor_json(binary_sensor::BinarySensor *obj, bool #ifdef USE_FAN void WebServer::on_fan_update(fan::Fan *obj) { - if (this->events_.count() == 0) + if (this->events_.empty()) return; - this->events_.send(this->fan_json(obj, DETAIL_STATE).c_str(), "state"); + this->events_.deferrable_send_state(obj, "state", fan_state_json_generator); } void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (fan::Fan *obj : App.get_fans()) { @@ -494,6 +660,12 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc } request->send(404); } +std::string WebServer::fan_state_json_generator(WebServer *web_server, void *source) { + return web_server->fan_json((fan::Fan *) (source), DETAIL_STATE); +} +std::string WebServer::fan_all_json_generator(WebServer *web_server, void *source) { + return web_server->fan_json((fan::Fan *) (source), DETAIL_ALL); +} std::string WebServer::fan_json(fan::Fan *obj, JsonDetail start_config) { return json::build_json([this, obj, start_config](JsonObject root) { set_json_icon_state_value(root, obj, "fan-" + obj->get_object_id(), obj->state ? "ON" : "OFF", obj->state, @@ -519,9 +691,9 @@ std::string WebServer::fan_json(fan::Fan *obj, JsonDetail start_config) { #ifdef USE_LIGHT void WebServer::on_light_update(light::LightState *obj) { - if (this->events_.count() == 0) + if (this->events_.empty()) return; - this->events_.send(this->light_json(obj, DETAIL_STATE).c_str(), "state"); + this->events_.deferrable_send_state(obj, "state", light_state_json_generator); } void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (light::LightState *obj : App.get_lights()) { @@ -613,6 +785,12 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa } request->send(404); } +std::string WebServer::light_state_json_generator(WebServer *web_server, void *source) { + return web_server->light_json((light::LightState *) (source), DETAIL_STATE); +} +std::string WebServer::light_all_json_generator(WebServer *web_server, void *source) { + return web_server->light_json((light::LightState *) (source), DETAIL_ALL); +} std::string WebServer::light_json(light::LightState *obj, JsonDetail start_config) { return json::build_json([this, obj, start_config](JsonObject root) { set_json_id(root, obj, "light-" + obj->get_object_id(), start_config); @@ -638,9 +816,9 @@ std::string WebServer::light_json(light::LightState *obj, JsonDetail start_confi #ifdef USE_COVER void WebServer::on_cover_update(cover::Cover *obj) { - if (this->events_.count() == 0) + if (this->events_.empty()) return; - this->events_.send(this->cover_json(obj, DETAIL_STATE).c_str(), "state"); + this->events_.deferrable_send_state(obj, "state", cover_state_json_generator); } void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (cover::Cover *obj : App.get_covers()) { @@ -698,6 +876,12 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa } request->send(404); } +std::string WebServer::cover_state_json_generator(WebServer *web_server, void *source) { + return web_server->cover_json((cover::Cover *) (source), DETAIL_STATE); +} +std::string WebServer::cover_all_json_generator(WebServer *web_server, void *source) { + return web_server->cover_json((cover::Cover *) (source), DETAIL_STATE); +} std::string WebServer::cover_json(cover::Cover *obj, JsonDetail start_config) { return json::build_json([this, obj, start_config](JsonObject root) { set_json_icon_state_value(root, obj, "cover-" + obj->get_object_id(), obj->is_fully_closed() ? "CLOSED" : "OPEN", @@ -722,9 +906,9 @@ std::string WebServer::cover_json(cover::Cover *obj, JsonDetail start_config) { #ifdef USE_NUMBER void WebServer::on_number_update(number::Number *obj, float state) { - if (this->events_.count() == 0) + if (this->events_.empty()) return; - this->events_.send(this->number_json(obj, state, DETAIL_STATE).c_str(), "state"); + this->events_.deferrable_send_state(obj, "state", number_state_json_generator); } void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_numbers()) { @@ -760,6 +944,12 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM request->send(404); } +std::string WebServer::number_state_json_generator(WebServer *web_server, void *source) { + return web_server->number_json((number::Number *) (source), ((number::Number *) (source))->state, DETAIL_STATE); +} +std::string WebServer::number_all_json_generator(WebServer *web_server, void *source) { + return web_server->number_json((number::Number *) (source), ((number::Number *) (source))->state, DETAIL_ALL); +} std::string WebServer::number_json(number::Number *obj, float value, JsonDetail start_config) { return json::build_json([this, obj, value, start_config](JsonObject root) { set_json_id(root, obj, "number-" + obj->get_object_id(), start_config); @@ -796,9 +986,9 @@ std::string WebServer::number_json(number::Number *obj, float value, JsonDetail #ifdef USE_DATETIME_DATE void WebServer::on_date_update(datetime::DateEntity *obj) { - if (this->events_.count() == 0) + if (this->events_.empty()) return; - this->events_.send(this->date_json(obj, DETAIL_STATE).c_str(), "state"); + this->events_.deferrable_send_state(obj, "state", date_state_json_generator); } void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_dates()) { @@ -827,7 +1017,7 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat } if (request->hasParam("value")) { - std::string value = request->getParam("value")->value().c_str(); + std::string value = request->getParam("value")->value().c_str(); // NOLINT call.set_date(value); } @@ -838,6 +1028,12 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat request->send(404); } +std::string WebServer::date_state_json_generator(WebServer *web_server, void *source) { + return web_server->date_json((datetime::DateEntity *) (source), DETAIL_STATE); +} +std::string WebServer::date_all_json_generator(WebServer *web_server, void *source) { + return web_server->date_json((datetime::DateEntity *) (source), DETAIL_ALL); +} std::string WebServer::date_json(datetime::DateEntity *obj, JsonDetail start_config) { return json::build_json([this, obj, start_config](JsonObject root) { set_json_id(root, obj, "date-" + obj->get_object_id(), start_config); @@ -858,9 +1054,9 @@ std::string WebServer::date_json(datetime::DateEntity *obj, JsonDetail start_con #ifdef USE_DATETIME_TIME void WebServer::on_time_update(datetime::TimeEntity *obj) { - if (this->events_.count() == 0) + if (this->events_.empty()) return; - this->events_.send(this->time_json(obj, DETAIL_STATE).c_str(), "state"); + this->events_.deferrable_send_state(obj, "state", time_state_json_generator); } void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_times()) { @@ -889,7 +1085,7 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat } if (request->hasParam("value")) { - std::string value = request->getParam("value")->value().c_str(); + std::string value = request->getParam("value")->value().c_str(); // NOLINT call.set_time(value); } @@ -899,6 +1095,12 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat } request->send(404); } +std::string WebServer::time_state_json_generator(WebServer *web_server, void *source) { + return web_server->time_json((datetime::TimeEntity *) (source), DETAIL_STATE); +} +std::string WebServer::time_all_json_generator(WebServer *web_server, void *source) { + return web_server->time_json((datetime::TimeEntity *) (source), DETAIL_ALL); +} std::string WebServer::time_json(datetime::TimeEntity *obj, JsonDetail start_config) { return json::build_json([this, obj, start_config](JsonObject root) { set_json_id(root, obj, "time-" + obj->get_object_id(), start_config); @@ -919,9 +1121,9 @@ std::string WebServer::time_json(datetime::TimeEntity *obj, JsonDetail start_con #ifdef USE_DATETIME_DATETIME void WebServer::on_datetime_update(datetime::DateTimeEntity *obj) { - if (this->events_.count() == 0) + if (this->events_.empty()) return; - this->events_.send(this->datetime_json(obj, DETAIL_STATE).c_str(), "state"); + this->events_.deferrable_send_state(obj, "state", datetime_state_json_generator); } void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_datetimes()) { @@ -950,7 +1152,7 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur } if (request->hasParam("value")) { - std::string value = request->getParam("value")->value().c_str(); + std::string value = request->getParam("value")->value().c_str(); // NOLINT call.set_datetime(value); } @@ -960,6 +1162,12 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur } request->send(404); } +std::string WebServer::datetime_state_json_generator(WebServer *web_server, void *source) { + return web_server->datetime_json((datetime::DateTimeEntity *) (source), DETAIL_STATE); +} +std::string WebServer::datetime_all_json_generator(WebServer *web_server, void *source) { + return web_server->datetime_json((datetime::DateTimeEntity *) (source), DETAIL_ALL); +} std::string WebServer::datetime_json(datetime::DateTimeEntity *obj, JsonDetail start_config) { return json::build_json([this, obj, start_config](JsonObject root) { set_json_id(root, obj, "datetime-" + obj->get_object_id(), start_config); @@ -981,9 +1189,9 @@ std::string WebServer::datetime_json(datetime::DateTimeEntity *obj, JsonDetail s #ifdef USE_TEXT void WebServer::on_text_update(text::Text *obj, const std::string &state) { - if (this->events_.count() == 0) + if (this->events_.empty()) return; - this->events_.send(this->text_json(obj, state, DETAIL_STATE).c_str(), "state"); + this->events_.deferrable_send_state(obj, "state", text_state_json_generator); } void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_texts()) { @@ -1008,7 +1216,7 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat auto call = obj->make_call(); if (request->hasParam("value")) { String value = request->getParam("value")->value(); - call.set_value(value.c_str()); + call.set_value(value.c_str()); // NOLINT } this->defer([call]() mutable { call.perform(); }); @@ -1018,6 +1226,12 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat request->send(404); } +std::string WebServer::text_state_json_generator(WebServer *web_server, void *source) { + return web_server->text_json((text::Text *) (source), ((text::Text *) (source))->state, DETAIL_STATE); +} +std::string WebServer::text_all_json_generator(WebServer *web_server, void *source) { + return web_server->text_json((text::Text *) (source), ((text::Text *) (source))->state, DETAIL_ALL); +} std::string WebServer::text_json(text::Text *obj, const std::string &value, JsonDetail start_config) { return json::build_json([this, obj, value, start_config](JsonObject root) { set_json_id(root, obj, "text-" + obj->get_object_id(), start_config); @@ -1045,9 +1259,9 @@ std::string WebServer::text_json(text::Text *obj, const std::string &value, Json #ifdef USE_SELECT void WebServer::on_select_update(select::Select *obj, const std::string &state, size_t index) { - if (this->events_.count() == 0) + if (this->events_.empty()) return; - this->events_.send(this->select_json(obj, state, DETAIL_STATE).c_str(), "state"); + this->events_.deferrable_send_state(obj, "state", select_state_json_generator); } void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_selects()) { @@ -1074,7 +1288,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM if (request->hasParam("option")) { auto option = request->getParam("option")->value(); - call.set_option(option.c_str()); // NOLINT(clang-diagnostic-deprecated-declarations) + call.set_option(option.c_str()); // NOLINT } this->schedule_([call]() mutable { call.perform(); }); @@ -1083,6 +1297,12 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM } request->send(404); } +std::string WebServer::select_state_json_generator(WebServer *web_server, void *source) { + return web_server->select_json((select::Select *) (source), ((select::Select *) (source))->state, DETAIL_STATE); +} +std::string WebServer::select_all_json_generator(WebServer *web_server, void *source) { + return web_server->select_json((select::Select *) (source), ((select::Select *) (source))->state, DETAIL_ALL); +} std::string WebServer::select_json(select::Select *obj, const std::string &value, JsonDetail start_config) { return json::build_json([this, obj, value, start_config](JsonObject root) { set_json_icon_state_value(root, obj, "select-" + obj->get_object_id(), value, value, start_config); @@ -1107,9 +1327,9 @@ std::string WebServer::select_json(select::Select *obj, const std::string &value #ifdef USE_CLIMATE void WebServer::on_climate_update(climate::Climate *obj) { - if (this->events_.count() == 0) + if (this->events_.empty()) return; - this->events_.send(this->climate_json(obj, DETAIL_STATE).c_str(), "state"); + this->events_.deferrable_send_state(obj, "state", climate_state_json_generator); } void WebServer::handle_climate_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_climates()) { @@ -1126,6 +1346,7 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url request->send(200, "application/json", data.c_str()); return; } + if (match.method != "set") { request->send(404); return; @@ -1135,17 +1356,17 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url if (request->hasParam("mode")) { auto mode = request->getParam("mode")->value(); - call.set_mode(mode.c_str()); + call.set_mode(mode.c_str()); // NOLINT } if (request->hasParam("fan_mode")) { auto mode = request->getParam("fan_mode")->value(); - call.set_fan_mode(mode.c_str()); + call.set_fan_mode(mode.c_str()); // NOLINT } if (request->hasParam("swing_mode")) { auto mode = request->getParam("swing_mode")->value(); - call.set_swing_mode(mode.c_str()); + call.set_swing_mode(mode.c_str()); // NOLINT } if (request->hasParam("target_temperature_high")) { @@ -1172,6 +1393,12 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url } request->send(404); } +std::string WebServer::climate_state_json_generator(WebServer *web_server, void *source) { + return web_server->climate_json((climate::Climate *) (source), DETAIL_STATE); +} +std::string WebServer::climate_all_json_generator(WebServer *web_server, void *source) { + return web_server->climate_json((climate::Climate *) (source), DETAIL_ALL); +} std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_config) { return json::build_json([this, obj, start_config](JsonObject root) { set_json_id(root, obj, "climate-" + obj->get_object_id(), start_config); @@ -1268,9 +1495,9 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf #ifdef USE_LOCK void WebServer::on_lock_update(lock::Lock *obj) { - if (this->events_.count() == 0) + if (this->events_.empty()) return; - this->events_.send(this->lock_json(obj, obj->state, DETAIL_STATE).c_str(), "state"); + this->events_.deferrable_send_state(obj, "state", lock_state_json_generator); } void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (lock::Lock *obj : App.get_locks()) { @@ -1301,6 +1528,12 @@ void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMat } request->send(404); } +std::string WebServer::lock_state_json_generator(WebServer *web_server, void *source) { + return web_server->lock_json((lock::Lock *) (source), ((lock::Lock *) (source))->state, DETAIL_STATE); +} +std::string WebServer::lock_all_json_generator(WebServer *web_server, void *source) { + return web_server->lock_json((lock::Lock *) (source), ((lock::Lock *) (source))->state, DETAIL_ALL); +} std::string WebServer::lock_json(lock::Lock *obj, lock::LockState value, JsonDetail start_config) { return json::build_json([this, obj, value, start_config](JsonObject root) { set_json_icon_state_value(root, obj, "lock-" + obj->get_object_id(), lock::lock_state_to_string(value), value, @@ -1319,9 +1552,9 @@ std::string WebServer::lock_json(lock::Lock *obj, lock::LockState value, JsonDet #ifdef USE_VALVE void WebServer::on_valve_update(valve::Valve *obj) { - if (this->events_.count() == 0) + if (this->events_.empty()) return; - this->events_.send(this->valve_json(obj, DETAIL_STATE).c_str(), "state"); + this->events_.deferrable_send_state(obj, "state", valve_state_json_generator); } void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (valve::Valve *obj : App.get_valves()) { @@ -1372,6 +1605,12 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa } request->send(404); } +std::string WebServer::valve_state_json_generator(WebServer *web_server, void *source) { + return web_server->valve_json((valve::Valve *) (source), DETAIL_STATE); +} +std::string WebServer::valve_all_json_generator(WebServer *web_server, void *source) { + return web_server->valve_json((valve::Valve *) (source), DETAIL_ALL); +} std::string WebServer::valve_json(valve::Valve *obj, JsonDetail start_config) { return json::build_json([this, obj, start_config](JsonObject root) { set_json_icon_state_value(root, obj, "valve-" + obj->get_object_id(), obj->is_fully_closed() ? "CLOSED" : "OPEN", @@ -1394,9 +1633,9 @@ std::string WebServer::valve_json(valve::Valve *obj, JsonDetail start_config) { #ifdef USE_ALARM_CONTROL_PANEL void WebServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) { - if (this->events_.count() == 0) + if (this->events_.empty()) return; - this->events_.send(this->alarm_control_panel_json(obj, obj->get_state(), DETAIL_STATE).c_str(), "state"); + this->events_.deferrable_send_state(obj, "state", alarm_control_panel_state_json_generator); } void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (alarm_control_panel::AlarmControlPanel *obj : App.get_alarm_control_panels()) { @@ -1416,7 +1655,7 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques auto call = obj->make_call(); if (request->hasParam("code")) { - call.set_code(request->getParam("code")->value().c_str()); + call.set_code(request->getParam("code")->value().c_str()); // NOLINT } if (match.method == "disarm") { @@ -1440,6 +1679,16 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques } request->send(404); } +std::string WebServer::alarm_control_panel_state_json_generator(WebServer *web_server, void *source) { + return web_server->alarm_control_panel_json((alarm_control_panel::AlarmControlPanel *) (source), + ((alarm_control_panel::AlarmControlPanel *) (source))->get_state(), + DETAIL_STATE); +} +std::string WebServer::alarm_control_panel_all_json_generator(WebServer *web_server, void *source) { + return web_server->alarm_control_panel_json((alarm_control_panel::AlarmControlPanel *) (source), + ((alarm_control_panel::AlarmControlPanel *) (source))->get_state(), + DETAIL_ALL); +} std::string WebServer::alarm_control_panel_json(alarm_control_panel::AlarmControlPanel *obj, alarm_control_panel::AlarmControlPanelState value, JsonDetail start_config) { @@ -1461,8 +1710,9 @@ std::string WebServer::alarm_control_panel_json(alarm_control_panel::AlarmContro #ifdef USE_EVENT void WebServer::on_event(event::Event *obj, const std::string &event_type) { - this->events_.send(this->event_json(obj, event_type, DETAIL_STATE).c_str(), "state"); + this->events_.deferrable_send_state(obj, "state", event_state_json_generator); } + void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (event::Event *obj : App.get_events()) { if (obj->get_object_id() != match.id) @@ -1481,6 +1731,14 @@ void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMa } request->send(404); } + +std::string WebServer::event_state_json_generator(WebServer *web_server, void *source) { + return web_server->event_json((event::Event *) (source), *(((event::Event *) (source))->last_event_type), + DETAIL_STATE); +} +std::string WebServer::event_all_json_generator(WebServer *web_server, void *source) { + return web_server->event_json((event::Event *) (source), *(((event::Event *) (source))->last_event_type), DETAIL_ALL); +} std::string WebServer::event_json(event::Event *obj, const std::string &event_type, JsonDetail start_config) { return json::build_json([this, obj, event_type, start_config](JsonObject root) { set_json_id(root, obj, "event-" + obj->get_object_id(), start_config); @@ -1506,9 +1764,9 @@ std::string WebServer::event_json(event::Event *obj, const std::string &event_ty #ifdef USE_UPDATE void WebServer::on_update(update::UpdateEntity *obj) { - if (this->events_.count() == 0) + if (this->events_.empty()) return; - this->events_.send(this->update_json(obj, DETAIL_STATE).c_str(), "state"); + this->events_.deferrable_send_state(obj, "state", update_state_json_generator); } void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (update::UpdateEntity *obj : App.get_updates()) { @@ -1537,6 +1795,12 @@ void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlM } request->send(404); } +std::string WebServer::update_state_json_generator(WebServer *web_server, void *source) { + return web_server->update_json((update::UpdateEntity *) (source), DETAIL_STATE); +} +std::string WebServer::update_all_json_generator(WebServer *web_server, void *source) { + return web_server->update_json((update::UpdateEntity *) (source), DETAIL_STATE); +} std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_config) { return json::build_json([this, obj, start_config](JsonObject root) { set_json_id(root, obj, "update-" + obj->get_object_id(), start_config); @@ -1575,6 +1839,12 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) { if (request->url() == "/") return true; +#ifdef USE_ARDUINO + if (request->url() == "/events") { + return true; + } +#endif + #ifdef USE_WEBSERVER_CSS_INCLUDE if (request->url() == "/0.css") return true; @@ -1597,7 +1867,7 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) { } #endif - UrlMatch match = match_url(request->url().c_str(), true); + UrlMatch match = match_url(request->url().c_str(), true); // NOLINT if (!match.valid) return false; #ifdef USE_SENSOR @@ -1708,6 +1978,13 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { return; } +#ifdef USE_ARDUINO + if (request->url() == "/events") { + this->events_.add_new_client(this, request); + return; + } +#endif + #ifdef USE_WEBSERVER_CSS_INCLUDE if (request->url() == "/0.css") { this->handle_css_request(request); @@ -1729,7 +2006,7 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { } #endif - UrlMatch match = match_url(request->url().c_str()); + UrlMatch match = match_url(request->url().c_str()); // NOLINT #ifdef USE_SENSOR if (match.domain == "sensor") { this->handle_sensor_request(request, match); diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 8edb678169..e4f044c50b 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -8,7 +8,11 @@ #include "esphome/core/controller.h" #include "esphome/core/entity_base.h" +#include +#include #include +#include +#include #include #ifdef USE_ESP32 #include @@ -54,6 +58,85 @@ struct SortingGroup { enum JsonDetail { DETAIL_ALL, DETAIL_STATE }; +/* + In order to defer updates in arduino mode, we need to create one AsyncEventSource per incoming request to /events. + This is because only minimal changes were made to the ESPAsyncWebServer lib_dep, it was undesirable to put deferred + update logic into that library. We need one deferred queue per connection so instead of one AsyncEventSource with + multiple clients, we have multiple event sources with one client each. This is slightly awkward which is why it's + implemented in a more straightforward way for ESP-IDF. Arudino platform will eventually go away and this workaround + can be forgotten. +*/ +#ifdef USE_ARDUINO +using message_generator_t = std::string(WebServer *, void *); + +class DeferredUpdateEventSourceList; +class DeferredUpdateEventSource : public AsyncEventSource { + friend class DeferredUpdateEventSourceList; + + /* + This class holds a pointer to the source component that wants to publish a state event, and a pointer to a function + that will lazily generate that event. The two pointers allow dedup in the deferred queue if multiple publishes for + the same component are backed up, and take up only 8 bytes of memory. The entry in the deferred queue (a + std::vector) is the DeferredEvent instance itself (not a pointer to one elsewhere in heap) so still only 8 bytes per + entry (and no heap fragmentation). Even 100 backed up events (you'd have to have at least 100 sensors publishing + because of dedup) would take up only 0.8 kB. + */ + struct DeferredEvent { + friend class DeferredUpdateEventSource; + + protected: + void *source_; + message_generator_t *message_generator_; + + public: + DeferredEvent(void *source, message_generator_t *message_generator) + : source_(source), message_generator_(message_generator) {} + bool operator==(const DeferredEvent &test) const { + return (source_ == test.source_ && message_generator_ == test.message_generator_); + } + } __attribute__((packed)); + + protected: + // surface a couple methods from the base class + using AsyncEventSource::handleRequest; + using AsyncEventSource::try_send; + + ListEntitiesIterator entities_iterator_; + // vector is used very specifically for its zero memory overhead even though items are popped from the front (memory + // footprint is more important than speed here) + std::vector deferred_queue_; + WebServer *web_server_; + + // helper for allowing only unique entries in the queue + void deq_push_back_with_dedup_(void *source, message_generator_t *message_generator); + + void process_deferred_queue_(); + + public: + DeferredUpdateEventSource(WebServer *ws, const String &url) + : AsyncEventSource(url), entities_iterator_(ListEntitiesIterator(ws, this)), web_server_(ws) {} + + void loop(); + + void deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator); + void try_send_nodefer(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0); +}; + +class DeferredUpdateEventSourceList : public std::list { + protected: + void on_client_connect_(WebServer *ws, DeferredUpdateEventSource *source); + void on_client_disconnect_(DeferredUpdateEventSource *source); + + public: + void loop(); + + void deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator); + void try_send_nodefer(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0); + + void add_new_client(WebServer *ws, AsyncWebServerRequest *request); +}; +#endif + /** This class allows users to create a web server with their ESP nodes. * * Behind the scenes it's using AsyncWebServer to set up the server. It exposes 3 things: @@ -64,6 +147,10 @@ enum JsonDetail { DETAIL_ALL, DETAIL_STATE }; * can be found under https://esphome.io/web-api/index.html. */ class WebServer : public Controller, public Component, public AsyncWebHandler { +#ifdef USE_ARDUINO + friend class DeferredUpdateEventSourceList; +#endif + public: WebServer(web_server_base::WebServerBase *base); @@ -153,6 +240,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a sensor request under '/sensor/'. void handle_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match); + static std::string sensor_state_json_generator(WebServer *web_server, void *source); + static std::string sensor_all_json_generator(WebServer *web_server, void *source); /// Dump the sensor state with its value as a JSON string. std::string sensor_json(sensor::Sensor *obj, float value, JsonDetail start_config); #endif @@ -163,6 +252,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a switch request under '/switch//'. void handle_switch_request(AsyncWebServerRequest *request, const UrlMatch &match); + static std::string switch_state_json_generator(WebServer *web_server, void *source); + static std::string switch_all_json_generator(WebServer *web_server, void *source); /// Dump the switch state with its value as a JSON string. std::string switch_json(switch_::Switch *obj, bool value, JsonDetail start_config); #endif @@ -171,6 +262,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a button request under '/button//press'. void handle_button_request(AsyncWebServerRequest *request, const UrlMatch &match); + static std::string button_state_json_generator(WebServer *web_server, void *source); + static std::string button_all_json_generator(WebServer *web_server, void *source); /// Dump the button details with its value as a JSON string. std::string button_json(button::Button *obj, JsonDetail start_config); #endif @@ -181,6 +274,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a binary sensor request under '/binary_sensor/'. void handle_binary_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match); + static std::string binary_sensor_state_json_generator(WebServer *web_server, void *source); + static std::string binary_sensor_all_json_generator(WebServer *web_server, void *source); /// Dump the binary sensor state with its value as a JSON string. std::string binary_sensor_json(binary_sensor::BinarySensor *obj, bool value, JsonDetail start_config); #endif @@ -191,6 +286,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a fan request under '/fan//'. void handle_fan_request(AsyncWebServerRequest *request, const UrlMatch &match); + static std::string fan_state_json_generator(WebServer *web_server, void *source); + static std::string fan_all_json_generator(WebServer *web_server, void *source); /// Dump the fan state as a JSON string. std::string fan_json(fan::Fan *obj, JsonDetail start_config); #endif @@ -201,6 +298,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a light request under '/light//'. void handle_light_request(AsyncWebServerRequest *request, const UrlMatch &match); + static std::string light_state_json_generator(WebServer *web_server, void *source); + static std::string light_all_json_generator(WebServer *web_server, void *source); /// Dump the light state as a JSON string. std::string light_json(light::LightState *obj, JsonDetail start_config); #endif @@ -211,6 +310,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a text sensor request under '/text_sensor/'. void handle_text_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match); + static std::string text_sensor_state_json_generator(WebServer *web_server, void *source); + static std::string text_sensor_all_json_generator(WebServer *web_server, void *source); /// Dump the text sensor state with its value as a JSON string. std::string text_sensor_json(text_sensor::TextSensor *obj, const std::string &value, JsonDetail start_config); #endif @@ -221,6 +322,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a cover request under '/cover//'. void handle_cover_request(AsyncWebServerRequest *request, const UrlMatch &match); + static std::string cover_state_json_generator(WebServer *web_server, void *source); + static std::string cover_all_json_generator(WebServer *web_server, void *source); /// Dump the cover state as a JSON string. std::string cover_json(cover::Cover *obj, JsonDetail start_config); #endif @@ -230,6 +333,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a number request under '/number/'. void handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match); + static std::string number_state_json_generator(WebServer *web_server, void *source); + static std::string number_all_json_generator(WebServer *web_server, void *source); /// Dump the number state with its value as a JSON string. std::string number_json(number::Number *obj, float value, JsonDetail start_config); #endif @@ -239,6 +344,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a date request under '/date/'. void handle_date_request(AsyncWebServerRequest *request, const UrlMatch &match); + static std::string date_state_json_generator(WebServer *web_server, void *source); + static std::string date_all_json_generator(WebServer *web_server, void *source); /// Dump the date state with its value as a JSON string. std::string date_json(datetime::DateEntity *obj, JsonDetail start_config); #endif @@ -248,6 +355,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a time request under '/time/'. void handle_time_request(AsyncWebServerRequest *request, const UrlMatch &match); + static std::string time_state_json_generator(WebServer *web_server, void *source); + static std::string time_all_json_generator(WebServer *web_server, void *source); /// Dump the time state with its value as a JSON string. std::string time_json(datetime::TimeEntity *obj, JsonDetail start_config); #endif @@ -257,6 +366,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a datetime request under '/datetime/'. void handle_datetime_request(AsyncWebServerRequest *request, const UrlMatch &match); + static std::string datetime_state_json_generator(WebServer *web_server, void *source); + static std::string datetime_all_json_generator(WebServer *web_server, void *source); /// Dump the datetime state with its value as a JSON string. std::string datetime_json(datetime::DateTimeEntity *obj, JsonDetail start_config); #endif @@ -266,6 +377,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a text input request under '/text/'. void handle_text_request(AsyncWebServerRequest *request, const UrlMatch &match); + static std::string text_state_json_generator(WebServer *web_server, void *source); + static std::string text_all_json_generator(WebServer *web_server, void *source); /// Dump the text state with its value as a JSON string. std::string text_json(text::Text *obj, const std::string &value, JsonDetail start_config); #endif @@ -275,6 +388,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a select request under '/select/'. void handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match); + static std::string select_state_json_generator(WebServer *web_server, void *source); + static std::string select_all_json_generator(WebServer *web_server, void *source); /// Dump the select state with its value as a JSON string. std::string select_json(select::Select *obj, const std::string &value, JsonDetail start_config); #endif @@ -284,6 +399,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a climate request under '/climate/'. void handle_climate_request(AsyncWebServerRequest *request, const UrlMatch &match); + static std::string climate_state_json_generator(WebServer *web_server, void *source); + static std::string climate_all_json_generator(WebServer *web_server, void *source); /// Dump the climate details std::string climate_json(climate::Climate *obj, JsonDetail start_config); #endif @@ -294,6 +411,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a lock request under '/lock//'. void handle_lock_request(AsyncWebServerRequest *request, const UrlMatch &match); + static std::string lock_state_json_generator(WebServer *web_server, void *source); + static std::string lock_all_json_generator(WebServer *web_server, void *source); /// Dump the lock state with its value as a JSON string. std::string lock_json(lock::Lock *obj, lock::LockState value, JsonDetail start_config); #endif @@ -304,6 +423,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a valve request under '/valve//'. void handle_valve_request(AsyncWebServerRequest *request, const UrlMatch &match); + static std::string valve_state_json_generator(WebServer *web_server, void *source); + static std::string valve_all_json_generator(WebServer *web_server, void *source); /// Dump the valve state as a JSON string. std::string valve_json(valve::Valve *obj, JsonDetail start_config); #endif @@ -314,6 +435,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a alarm_control_panel request under '/alarm_control_panel/'. void handle_alarm_control_panel_request(AsyncWebServerRequest *request, const UrlMatch &match); + static std::string alarm_control_panel_state_json_generator(WebServer *web_server, void *source); + static std::string alarm_control_panel_all_json_generator(WebServer *web_server, void *source); /// Dump the alarm_control_panel state with its value as a JSON string. std::string alarm_control_panel_json(alarm_control_panel::AlarmControlPanel *obj, alarm_control_panel::AlarmControlPanelState value, JsonDetail start_config); @@ -322,6 +445,9 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { #ifdef USE_EVENT void on_event(event::Event *obj, const std::string &event_type) override; + static std::string event_state_json_generator(WebServer *web_server, void *source); + static std::string event_all_json_generator(WebServer *web_server, void *source); + /// Handle a event request under '/event'. void handle_event_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -335,6 +461,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a update request under '/update/'. void handle_update_request(AsyncWebServerRequest *request, const UrlMatch &match); + static std::string update_state_json_generator(WebServer *web_server, void *source); + static std::string update_all_json_generator(WebServer *web_server, void *source); /// Dump the update state with its value as a JSON string. std::string update_json(update::UpdateEntity *obj, JsonDetail start_config); #endif @@ -349,14 +477,19 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { void add_entity_config(EntityBase *entity, float weight, uint64_t group); void add_sorting_group(uint64_t group_id, const std::string &group_name, float weight); - protected: - void schedule_(std::function &&f); - friend ListEntitiesIterator; - web_server_base::WebServerBase *base_; - AsyncEventSource events_{"/events"}; - ListEntitiesIterator entities_iterator_; std::map sorting_entitys_; std::map sorting_groups_; + bool include_internal_{false}; + + protected: + void schedule_(std::function &&f); + web_server_base::WebServerBase *base_; +#ifdef USE_ARDUINO + DeferredUpdateEventSourceList events_; +#endif +#ifdef USE_ESP_IDF + AsyncEventSource events_{"/events", this}; +#endif #if USE_WEBSERVER_VERSION == 1 const char *css_url_{nullptr}; @@ -368,7 +501,6 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { #ifdef USE_WEBSERVER_JS_INCLUDE const char *js_include_{nullptr}; #endif - bool include_internal_{false}; bool allow_ota_{true}; bool expose_log_{true}; #ifdef USE_ESP32 diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py index 4f894619b0..115f521d04 100644 --- a/esphome/components/web_server_base/__init__.py +++ b/esphome/components/web_server_base/__init__.py @@ -37,4 +37,4 @@ async def to_code(config): cg.add_library("FS", None) cg.add_library("Update", None) # https://github.com/esphome/ESPAsyncWebServer/blob/master/library.json - cg.add_library("esphome/ESPAsyncWebServer-esphome", "3.2.2") + cg.add_library("esphome/ESPAsyncWebServer-esphome", "3.3.0") diff --git a/esphome/components/web_server_idf/__init__.py b/esphome/components/web_server_idf/__init__.py index bd3db24bc6..a84d9bb663 100644 --- a/esphome/components/web_server_idf/__init__.py +++ b/esphome/components/web_server_idf/__init__.py @@ -8,6 +8,8 @@ CONFIG_SCHEMA = cv.All( cv.only_with_esp_idf, ) +AUTO_LOAD = ["web_server"] + async def to_code(config): # Increase the maximum supported size of headers section in HTTP request packet to be processed by the server diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index cf187cd647..428bd262e8 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -8,6 +8,10 @@ #include "esp_tls_crypto.h" #include "utils.h" + +#include "esphome/components/web_server/web_server.h" +#include "esphome/components/web_server/list_entities.h" + #include "web_server_idf.h" namespace esphome { @@ -276,21 +280,37 @@ AsyncEventSource::~AsyncEventSource() { } void AsyncEventSource::handleRequest(AsyncWebServerRequest *request) { - auto *rsp = new AsyncEventSourceResponse(request, this); // NOLINT(cppcoreguidelines-owning-memory) + auto *rsp = // NOLINT(cppcoreguidelines-owning-memory) + new AsyncEventSourceResponse(request, this, this->web_server_); if (this->on_connect_) { this->on_connect_(rsp); } this->sessions_.insert(rsp); } -void AsyncEventSource::send(const char *message, const char *event, uint32_t id, uint32_t reconnect) { +void AsyncEventSource::loop() { for (auto *ses : this->sessions_) { - ses->send(message, event, id, reconnect); + ses->loop(); } } -AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest *request, AsyncEventSource *server) - : server_(server) { +void AsyncEventSource::try_send_nodefer(const char *message, const char *event, uint32_t id, uint32_t reconnect) { + for (auto *ses : this->sessions_) { + ses->try_send_nodefer(message, event, id, reconnect); + } +} + +void AsyncEventSource::deferrable_send_state(void *source, const char *event_type, + message_generator_t *message_generator) { + for (auto *ses : this->sessions_) { + ses->deferrable_send_state(source, event_type, message_generator); + } +} + +AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest *request, + esphome::web_server_idf::AsyncEventSource *server, + esphome::web_server::WebServer *ws) + : server_(server), web_server_(ws), entities_iterator_(new esphome::web_server::ListEntitiesIterator(ws, server)) { httpd_req_t *req = *request; httpd_resp_set_status(req, HTTPD_200); @@ -309,6 +329,30 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest * this->hd_ = req->handle; this->fd_ = httpd_req_to_sockfd(req); + + // Configure reconnect timeout and send config + // this should always go through since the tcp send buffer is empty on connect + std::string message = ws->get_config_json(); + this->try_send_nodefer(message.c_str(), "ping", millis(), 30000); + + for (auto &group : ws->sorting_groups_) { + message = json::build_json([group](JsonObject root) { + root["name"] = group.second.name; + root["sorting_weight"] = group.second.weight; + }); + + // a (very) large number of these should be able to be queued initially without defer + // since the only thing in the send buffer at this point is the initial ping/config + this->try_send_nodefer(message.c_str(), "sorting_group"); + } + + this->entities_iterator_->begin(ws->include_internal_); + + // just dump them all up-front and take advantage of the deferred queue + // on second thought that takes too long, but leaving the commented code here for debug purposes + // while(!this->entities_iterator_->completed()) { + // this->entities_iterator_->advance(); + //} } void AsyncEventSourceResponse::destroy(void *ptr) { @@ -317,52 +361,155 @@ void AsyncEventSourceResponse::destroy(void *ptr) { delete rsp; // NOLINT(cppcoreguidelines-owning-memory) } -void AsyncEventSourceResponse::send(const char *message, const char *event, uint32_t id, uint32_t reconnect) { - if (this->fd_ == 0) { +// helper for allowing only unique entries in the queue +void AsyncEventSourceResponse::deq_push_back_with_dedup_(void *source, message_generator_t *message_generator) { + DeferredEvent item(source, message_generator); + + auto iter = std::find_if(this->deferred_queue_.begin(), this->deferred_queue_.end(), + [&item](const DeferredEvent &test) -> bool { return test == item; }); + + if (iter != this->deferred_queue_.end()) { + (*iter) = item; + } else { + this->deferred_queue_.push_back(item); + } +} + +void AsyncEventSourceResponse::process_deferred_queue_() { + while (!deferred_queue_.empty()) { + DeferredEvent &de = deferred_queue_.front(); + std::string message = de.message_generator_(web_server_, de.source_); + if (this->try_send_nodefer(message.c_str(), "state")) { + // O(n) but memory efficiency is more important than speed here which is why std::vector was chosen + deferred_queue_.erase(deferred_queue_.begin()); + } else { + break; + } + } +} + +void AsyncEventSourceResponse::process_buffer_() { + if (event_buffer_.empty()) { + return; + } + if (event_bytes_sent_ == event_buffer_.size()) { + event_buffer_.resize(0); + event_bytes_sent_ = 0; return; } - std::string ev; + int bytes_sent = httpd_socket_send(this->hd_, this->fd_, event_buffer_.c_str() + event_bytes_sent_, + event_buffer_.size() - event_bytes_sent_, 0); + if (bytes_sent == HTTPD_SOCK_ERR_TIMEOUT || bytes_sent == HTTPD_SOCK_ERR_FAIL) { + return; + } + event_bytes_sent_ += bytes_sent; + + if (event_bytes_sent_ == event_buffer_.size()) { + event_buffer_.resize(0); + event_bytes_sent_ = 0; + } +} + +void AsyncEventSourceResponse::loop() { + process_buffer_(); + process_deferred_queue_(); + if (!this->entities_iterator_->completed()) + this->entities_iterator_->advance(); +} + +bool AsyncEventSourceResponse::try_send_nodefer(const char *message, const char *event, uint32_t id, + uint32_t reconnect) { + if (this->fd_ == 0) { + return false; + } + + process_buffer_(); + if (!event_buffer_.empty()) { + // there is still pending event data to send first + return false; + } + + // 8 spaces are standing in for the hexidecimal chunk length to print later + const char chunk_len_header[] = " " CRLF_STR; + const int chunk_len_header_len = sizeof(chunk_len_header) - 1; + + event_buffer_.append(chunk_len_header); if (reconnect) { - ev.append("retry: ", sizeof("retry: ") - 1); - ev.append(to_string(reconnect)); - ev.append(CRLF_STR, CRLF_LEN); + event_buffer_.append("retry: ", sizeof("retry: ") - 1); + event_buffer_.append(to_string(reconnect)); + event_buffer_.append(CRLF_STR, CRLF_LEN); } if (id) { - ev.append("id: ", sizeof("id: ") - 1); - ev.append(to_string(id)); - ev.append(CRLF_STR, CRLF_LEN); + event_buffer_.append("id: ", sizeof("id: ") - 1); + event_buffer_.append(to_string(id)); + event_buffer_.append(CRLF_STR, CRLF_LEN); } if (event && *event) { - ev.append("event: ", sizeof("event: ") - 1); - ev.append(event); - ev.append(CRLF_STR, CRLF_LEN); + event_buffer_.append("event: ", sizeof("event: ") - 1); + event_buffer_.append(event); + event_buffer_.append(CRLF_STR, CRLF_LEN); } if (message && *message) { - ev.append("data: ", sizeof("data: ") - 1); - ev.append(message); - ev.append(CRLF_STR, CRLF_LEN); + event_buffer_.append("data: ", sizeof("data: ") - 1); + event_buffer_.append(message); + event_buffer_.append(CRLF_STR, CRLF_LEN); } - if (ev.empty()) { + if (event_buffer_.empty()) { + return true; + } + + event_buffer_.append(CRLF_STR, CRLF_LEN); + event_buffer_.append(CRLF_STR, CRLF_LEN); + + // chunk length header itself and the final chunk terminating CRLF are not counted as part of the chunk + int chunk_len = event_buffer_.size() - CRLF_LEN - chunk_len_header_len; + char chunk_len_str[9]; + snprintf(chunk_len_str, 9, "%08x", chunk_len); + std::memcpy(&event_buffer_[0], chunk_len_str, 8); + + event_bytes_sent_ = 0; + process_buffer_(); + + return true; +} + +void AsyncEventSourceResponse::deferrable_send_state(void *source, const char *event_type, + message_generator_t *message_generator) { + // allow all json "details_all" to go through before publishing bare state events, this avoids unnamed entries showing + // up in the web GUI and reduces event load during initial connect + if (!entities_iterator_->completed() && 0 != strcmp(event_type, "state_detail_all")) return; + + if (source == nullptr) + return; + if (event_type == nullptr) + return; + if (message_generator == nullptr) + return; + + if (0 != strcmp(event_type, "state_detail_all") && 0 != strcmp(event_type, "state")) { + ESP_LOGE(TAG, "Can't defer non-state event"); } - ev.append(CRLF_STR, CRLF_LEN); + process_buffer_(); + process_deferred_queue_(); - // Sending chunked content prelude - auto cs = str_snprintf("%x" CRLF_STR, 4 * sizeof(ev.size()) + CRLF_LEN, ev.size()); - httpd_socket_send(this->hd_, this->fd_, cs.c_str(), cs.size(), 0); - - // Sendiing content chunk - httpd_socket_send(this->hd_, this->fd_, ev.c_str(), ev.size(), 0); - - // Indicate end of chunk - httpd_socket_send(this->hd_, this->fd_, CRLF_STR, CRLF_LEN, 0); + if (!event_buffer_.empty() || !deferred_queue_.empty()) { + // outgoing event buffer or deferred queue still not empty which means downstream tcp send buffer full, no point + // trying to send first + deq_push_back_with_dedup_(source, message_generator); + } else { + std::string message = message_generator(web_server_, source); + if (!this->try_send_nodefer(message.c_str(), "state")) { + deq_push_back_with_dedup_(source, message_generator); + } + } } } // namespace web_server_idf diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index 2ead5e3f03..13a3ef168d 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -4,12 +4,18 @@ #include #include +#include #include #include #include +#include #include namespace esphome { +namespace web_server { +class WebServer; +class ListEntitiesIterator; +}; // namespace web_server namespace web_server_idf { #define F(string_literal) (string_literal) @@ -215,19 +221,58 @@ class AsyncWebHandler { }; class AsyncEventSource; +class AsyncEventSourceResponse; + +using message_generator_t = std::string(esphome::web_server::WebServer *, void *); + +/* + This class holds a pointer to the source component that wants to publish a state event, and a pointer to a function + that will lazily generate that event. The two pointers allow dedup in the deferred queue if multiple publishes for + the same component are backed up, and take up only 8 bytes of memory. The entry in the deferred queue (a + std::vector) is the DeferredEvent instance itself (not a pointer to one elsewhere in heap) so still only 8 bytes per + entry (and no heap fragmentation). Even 100 backed up events (you'd have to have at least 100 sensors publishing + because of dedup) would take up only 0.8 kB. +*/ +struct DeferredEvent { + friend class AsyncEventSourceResponse; + + protected: + void *source_; + message_generator_t *message_generator_; + + public: + DeferredEvent(void *source, message_generator_t *message_generator) + : source_(source), message_generator_(message_generator) {} + bool operator==(const DeferredEvent &test) const { + return (source_ == test.source_ && message_generator_ == test.message_generator_); + } +} __attribute__((packed)); class AsyncEventSourceResponse { friend class AsyncEventSource; public: - void send(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0); + bool try_send_nodefer(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0); + void deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator); + void loop(); protected: - AsyncEventSourceResponse(const AsyncWebServerRequest *request, AsyncEventSource *server); + AsyncEventSourceResponse(const AsyncWebServerRequest *request, esphome::web_server_idf::AsyncEventSource *server, + esphome::web_server::WebServer *ws); + + void deq_push_back_with_dedup_(void *source, message_generator_t *message_generator); + void process_deferred_queue_(); + void process_buffer_(); + static void destroy(void *p); AsyncEventSource *server_; httpd_handle_t hd_{}; int fd_{}; + std::vector deferred_queue_; + esphome::web_server::WebServer *web_server_; + std::unique_ptr entities_iterator_; + std::string event_buffer_{""}; + size_t event_bytes_sent_; }; using AsyncEventSourceClient = AsyncEventSourceResponse; @@ -237,7 +282,7 @@ class AsyncEventSource : public AsyncWebHandler { using connect_handler_t = std::function; public: - AsyncEventSource(std::string url) : url_(std::move(url)) {} + AsyncEventSource(std::string url, esphome::web_server::WebServer *ws) : url_(std::move(url)), web_server_(ws) {} ~AsyncEventSource() override; // NOLINTNEXTLINE(readability-identifier-naming) @@ -249,7 +294,10 @@ class AsyncEventSource : public AsyncWebHandler { // NOLINTNEXTLINE(readability-identifier-naming) void onConnect(connect_handler_t cb) { this->on_connect_ = std::move(cb); } - void send(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0); + void try_send_nodefer(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0); + void deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator); + void loop(); + bool empty() { return this->count() == 0; } size_t count() const { return this->sessions_.size(); } @@ -257,6 +305,7 @@ class AsyncEventSource : public AsyncWebHandler { std::string url_; std::set sessions_; connect_handler_t on_connect_{}; + esphome::web_server::WebServer *web_server_; }; class DefaultHeaders { diff --git a/platformio.ini b/platformio.ini index 4153310480..69a0b7ce2a 100644 --- a/platformio.ini +++ b/platformio.ini @@ -62,7 +62,7 @@ lib_deps = SPI ; spi (Arduino built-in) Wire ; i2c (Arduino built-int) heman/AsyncMqttClient-esphome@1.0.0 ; mqtt - esphome/ESPAsyncWebServer-esphome@3.2.2 ; web_server_base + esphome/ESPAsyncWebServer-esphome@3.3.0 ; web_server_base fastled/FastLED@3.3.2 ; fastled_base mikalhart/TinyGPSPlus@1.0.2 ; gps freekode/TM1651@1.0.1 ; tm1651