From f1b0674d41918819985424b9f2d443ac132efd9b Mon Sep 17 00:00:00 2001 From: BlueAndi Date: Tue, 7 Jan 2025 15:20:56 +0100 Subject: [PATCH] Disable HomeAssistant MQTT discovery in case of an filesystem update to avoid that the welcome plugin will be recognized by HA after the restart and before the user can restore the settings. --- src/Update/UpdateMgr.cpp | 215 +++++++++++++++++++++++---------------- src/Update/UpdateMgr.h | 24 ++++- src/Web/Pages.cpp | 102 +++++++++---------- 3 files changed, 198 insertions(+), 143 deletions(-) diff --git a/src/Update/UpdateMgr.cpp b/src/Update/UpdateMgr.cpp index 50fd710c..64931420 100644 --- a/src/Update/UpdateMgr.cpp +++ b/src/Update/UpdateMgr.cpp @@ -35,7 +35,6 @@ #include "UpdateMgr.h" #include -#include #include #include @@ -124,12 +123,12 @@ void UpdateMgr::begin() { if (true == m_isInitialized) { + String hostname = ArduinoOTA.getHostname(); + /* Start over-the-air server */ ArduinoOTA.begin(); - LOG_INFO(String("OTA hostname: ") + ArduinoOTA.getHostname()); - LOG_INFO(String("Sketch size: ") + ESP.getSketchSize() + " bytes"); - LOG_INFO(String("Free size: ") + ESP.getFreeSketchSpace() + " bytes"); + LOG_INFO("Arduino-OTA ready (hostname: %s).", hostname.c_str()); } } @@ -158,7 +157,7 @@ void UpdateMgr::process() } } -void UpdateMgr::beginProgress() +void UpdateMgr::prepareUpdate(bool isFilesystemUpdate) { if (true == m_isInitialized) { @@ -176,13 +175,67 @@ void UpdateMgr::beginProgress() /* Unregister all plugin topics (no purge). */ PluginMgr::getInstance().unregisterAllPluginTopics(); + /* Disable HomeAssistant MQTT automatic discovery to avoid that the welcome plugin + * will be discovered, after a filesystem update. + */ + if (true == isFilesystemUpdate) + { + SettingsService& settings = SettingsService::getInstance(); + + /* Key see HomeAssistantMqtt::KEY_HA_DISCOVERY_ENABLE + * Include the header is not possible, because MQTT might not be compiled in. + */ + KeyValue* kvHomeAssistantEnableDiscovery = settings.getSettingByKey("ha_ena"); + + if ((nullptr != kvHomeAssistantEnableDiscovery) && + (KeyValue::TYPE_BOOL == kvHomeAssistantEnableDiscovery->getValueType())) + { + if (true == settings.open(false)) + { + KeyValueBool* homeAssistantEnableDiscovery = static_cast(kvHomeAssistantEnableDiscovery); + + homeAssistantEnableDiscovery->setValue(false); + settings.close(); + + LOG_INFO("HA discovery disabled for filesystem update."); + } + } + } + /* Stop services, but keep webserver running! */ Services::stopAll(); + if (true == isFilesystemUpdate) + { + /* Close filesystem before continue. + * Note, this needs a restart after update is finished. + */ + FILESYSTEM.end(); + } + m_updateIsRunning = true; - m_progress = UINT8_MAX; /* Force update. */ + m_progress = UINT8_MAX; /* Force progress update of inital value. */ m_textWidget.setFormatStr("Update"); + } +} +void UpdateMgr::prepareForRestart() +{ + getInstance().m_updateIsRunning = false; + + /* Mount filesystem, because it may be unmounted because of an filesystem + * update. + */ + if (false == FILESYSTEM.begin()) + { + LOG_FATAL("Couldn't mount filesystem."); + } +} + +void UpdateMgr::beginProgress() +{ + if (true == m_updateIsRunning) + { /* Show user update status */ updateProgress(0U); } @@ -190,31 +243,12 @@ void UpdateMgr::beginProgress() void UpdateMgr::updateProgress(uint8_t progress) { - if ((true == m_isInitialized) && + if ((true == m_updateIsRunning) && (m_progress != progress)) { - Display& display = Display::getInstance(); - - m_progress = progress; - + m_progress = progress; m_progressBar.setProgress(m_progress); - - /* Update display manually. Note, that this must be done to avoid - * artifacts on the display, caused by long flash write cycles. - */ - display.fillScreen(ColorDef::BLACK); - m_progressBar.update(display); /* Draw the progress bar in the background. */ - m_textWidget.update(display); /* Overlay with the text. */ - display.show(); - - /* Wait until the LED matrix is updated to avoid artifacts on the - * display. - */ - while (false == display.isReady()) - { - /* Just wait and give other tasks a chance. */ - delay(1U); - } + updateDisplay(true); /* Show update status on console. */ LOG_INFO(String("[") + m_progress + "%]"); @@ -223,26 +257,11 @@ void UpdateMgr::updateProgress(uint8_t progress) void UpdateMgr::endProgress() { - Display& display = Display::getInstance(); - - display.fillScreen(ColorDef::BLACK); - m_textWidget.setFormatStr("..."); - m_textWidget.update(display); - display.show(); - - /* Wait until the LED matrix is updated to avoid artifacts on the - * display. - */ - while (false == display.isReady()) + if (true == m_updateIsRunning) { - /* Just wait and give other tasks a chance. */ - delay(1U); + m_textWidget.setFormatStr("..."); + updateDisplay(false); } - - /* Don't start the services again, because - * - a restart will come anyway and - * - starting the services would notify the online status for a short moment, until the restart happens. - */ } /****************************************************************************** @@ -273,9 +292,35 @@ UpdateMgr::~UpdateMgr() { } +void UpdateMgr::updateDisplay(bool showProgress) +{ + Display& display = Display::getInstance(); + + /* Update display manually. Note, that this must be done to avoid + * artifacts on the display, caused by long flash write cycles. + */ + display.fillScreen(ColorDef::BLACK); + if (true == showProgress) + { + m_progressBar.update(display); /* Draw the progress bar in the background. */ + } + m_textWidget.update(display); /* Overlay with the text. */ + display.show(); + + /* Wait until the LED matrix is updated to avoid artifacts on the + * display. + */ + while (false == display.isReady()) + { + /* Just wait and give other tasks a chance. */ + delay(1U); + } +} + void UpdateMgr::onStart() { - String infoStr = "Start OTA update of "; + String infoStr = "Start OTA update of "; + bool isFilesystemUpdate = false; /* Shall the firmware be updated? */ if (U_FLASH == ArduinoOTA.getCommand()) @@ -285,7 +330,9 @@ void UpdateMgr::onStart() /* The filesystem will be updated. */ else if (U_SPIFFS == ArduinoOTA.getCommand()) { - infoStr += "filesystem."; + infoStr += "filesystem."; + + isFilesystemUpdate = true; } else { @@ -294,19 +341,15 @@ void UpdateMgr::onStart() LOG_INFO(infoStr); - getInstance().beginProgress(); - - /* Stop webserver, before filesystem may be unmounted. */ + /* Stop webserver, before filesystem may be unmounted. + * This can not be moved to prepareUpdate(), because the update may come + * via the webserver. Therefore it can only be stopped in case of + * ArudinoOTA. + */ MyWebServer::end(); - /* Shall the filesystem will be updated? */ - if (U_SPIFFS == ArduinoOTA.getCommand()) - { - /* Close filesystem before continue. - * Note, this needs a restart after update is finished. - */ - FILESYSTEM.end(); - } + getInstance().prepareUpdate(isFilesystemUpdate); + getInstance().beginProgress(); } void UpdateMgr::onEnd() @@ -315,6 +358,7 @@ void UpdateMgr::onEnd() getInstance().m_updateIsRunning = false; getInstance().endProgress(); + getInstance().prepareForRestart(); /* Note, there is no need here to start the webserver or the display * manager again, because we request a restart of the system now. @@ -331,67 +375,62 @@ void UpdateMgr::onProgress(unsigned int progress, unsigned int total) void UpdateMgr::onError(ota_error_t error) { - String infoStr; + const uint32_t RESTART_DELAY = 4000U; /* ms */ + String errorStr; + /* Keep error information short to avoid that text scrolling is needed. + * Because the display manager is stopped during the update. + */ switch (error) { case OTA_AUTH_ERROR: - infoStr = "OTA - Authentication error."; + errorStr = "EAuth"; break; case OTA_BEGIN_ERROR: - infoStr = "OTA - Begin error."; + errorStr = "EBegin"; break; case OTA_CONNECT_ERROR: - infoStr = "OTA - Connect error."; + errorStr = "EErr"; break; case OTA_RECEIVE_ERROR: - infoStr = "OTA - Receive error."; + errorStr = "ERcv"; break; case OTA_END_ERROR: - infoStr = "OTA - End error."; + errorStr = "EEnd"; break; default: - infoStr = "OTA - Unknown error."; + errorStr = "EUndef"; break; } - LOG_INFO(infoStr); + LOG_ERROR(errorStr); - /* Mount filesystem, because it may be unmounted in case of failed filesystem update. */ - if (false == FILESYSTEM.begin()) + /* If the authentication fails, the onError() is called and there is no + * running update. Therefore no restart is necessary, just notify + * the user. + */ + if (false == getInstance().m_updateIsRunning) { - /* To ensure the log information will be shown. */ - const uint32_t RESTART_DELAY = 100U; /* ms */ - - LOG_FATAL("Couldn't mount filesystem."); + const uint32_t DURATION_NON_SCROLLING = 4000U; /* ms */ + const uint32_t SCROLLING_REPEAT_NUM = 1U; - getInstance().reqRestart(RESTART_DELAY); + SysMsg::getInstance().show(errorStr, DURATION_NON_SCROLLING, SCROLLING_REPEAT_NUM); } else { getInstance().endProgress(); + getInstance().prepareForRestart(); - /* Reset only if the error happened during update. - * Security note: This avoids a reset in case the authentication failed. - */ - if (true == getInstance().m_updateIsRunning) - { - const uint32_t DURATION_NON_SCROLLING = 4000U; /* ms */ - const uint32_t SCROLLING_REPEAT_NUM = 2U; + getInstance().m_textWidget.setFormatStr(errorStr); + getInstance().updateDisplay(false); - SysMsg::getInstance().show(infoStr, DURATION_NON_SCROLLING, SCROLLING_REPEAT_NUM); - - /* Request a restart */ - getInstance().reqRestart(DURATION_NON_SCROLLING); - } + getInstance().reqRestart(RESTART_DELAY); } - - getInstance().m_updateIsRunning = false; } /****************************************************************************** diff --git a/src/Update/UpdateMgr.h b/src/Update/UpdateMgr.h index aa172044..869bd794 100644 --- a/src/Update/UpdateMgr.h +++ b/src/Update/UpdateMgr.h @@ -137,10 +137,23 @@ class UpdateMgr } } + /** + * Prepare the system for an update. + * + * @param[in] isFilesystemUpdate Is it a filesystem update? + */ + void prepareUpdate(bool isFilesystemUpdate); + + /** + * Prepare the system for a restart after an successful or even + * and failed update. + */ + void prepareForRestart(); + /** * Show the user that the update starts. */ - void beginProgress(void); + void beginProgress(); /** * Show the user the current update progress. @@ -152,7 +165,7 @@ class UpdateMgr /** * Show the user that the update is finished. */ - void endProgress(void); + void endProgress(); /** Over-the-air update password */ static const char* OTA_PASSWORD; @@ -198,6 +211,13 @@ class UpdateMgr */ ~UpdateMgr(); + /** + * Update the display content. + * + * @param[in] showProgress If true, the progress bar will be shown too. + */ + void updateDisplay(bool showProgress); + /** * Over-the-air update start. */ diff --git a/src/Web/Pages.cpp b/src/Web/Pages.cpp index cd5e4f2d..5dc7e326 100644 --- a/src/Web/Pages.cpp +++ b/src/Web/Pages.cpp @@ -418,6 +418,9 @@ static void uploadPage(AsyncWebServerRequest* request) /* Trigger restart after the client has disconnected. * Do this in every case to ensure that if there was any error, the * device will be restarted as well. + * + * Requesting a restart after the client has disconnected, is necessary to be + * able to update more than just on file. */ request->onDisconnect( []() { @@ -437,10 +440,15 @@ static void uploadPage(AsyncWebServerRequest* request) */ static void uploadHandler(AsyncWebServerRequest* request, const String& filename, size_t index, uint8_t* data, size_t len, bool final) { + UpdateMgr& updateMgr = UpdateMgr::getInstance(); + /* Begin of upload? */ if (0 == index) { - uint32_t fileSize = UPDATE_SIZE_UNKNOWN; + uint32_t fileSize = UPDATE_SIZE_UNKNOWN; + int cmd = U_FLASH; + AsyncWebHeader* headerXFileSize = nullptr; + bool isFilesystemUpdate = false; /* If there is a pending upload, abort it. */ if (true == Update.isRunning()) @@ -450,9 +458,6 @@ static void uploadHandler(AsyncWebServerRequest* request, const String& filename } /* Upload firmware, bootloader or filesystem? */ - int cmd = U_FLASH; - AsyncWebHeader* headerXFileSize = nullptr; - if (filename == FIRMWARE_FILENAME) { cmd = U_FLASH; @@ -465,8 +470,9 @@ static void uploadHandler(AsyncWebServerRequest* request, const String& filename } else if (filename == FILESYSTEM_FILENAME) { - cmd = U_SPIFFS; - headerXFileSize = request->getHeader("X-File-Size-Filesystem"); + cmd = U_SPIFFS; + headerXFileSize = request->getHeader("X-File-Size-Filesystem"); + isFilesystemUpdate = true; } else { @@ -492,92 +498,82 @@ static void uploadHandler(AsyncWebServerRequest* request, const String& filename gIsUploadError = false; - /* Update filesystem? */ - if (U_SPIFFS == cmd) - { - /* Close filesystem before continue. */ - FILESYSTEM.end(); - } - - /* Start update */ + /* Start update, after the update procedure is prepared! */ if (false == Update.begin(fileSize, cmd)) { LOG_ERROR("Upload failed: %s", Update.errorString()); gIsUploadError = true; - /* Mount filesystem again, it may be unmounted in case of filesystem update.*/ - if (false == FILESYSTEM.begin()) - { - LOG_FATAL("Couldn't mount filesystem."); - } - /* Inform client about abort.*/ request->send(HttpStatus::STATUS_CODE_PAYLOAD_TOO_LARGE, "text/plain", "Upload aborted."); } /* Update is now running. */ else { + /* Prepare the update procedure. */ + updateMgr.prepareUpdate(isFilesystemUpdate); + /* Use UpdateMgr to show the user the update status. * Note, the display manager will be completely stopped during this, * to avoid artifacts on the display, because of long writes to flash. */ - UpdateMgr::getInstance().beginProgress(); + updateMgr.beginProgress(); } } + /* Is update in progress? */ if (true == Update.isRunning()) { + /* Continue update procedure. */ if (false == gIsUploadError) { if (len != Update.write(data, len)) { - LOG_ERROR("Upload failed: %s", Update.errorString()); gIsUploadError = true; } else { uint32_t progress = (Update.progress() * 100) / Update.size(); - UpdateMgr::getInstance().updateProgress(progress); - } + updateMgr.updateProgress(progress); - /* Upload finished? */ - if (true == final) - { - /* Finish update now. */ - if (false == Update.end(true)) + /* Upload finished? */ + if (true == final) { - LOG_ERROR("Upload failed: %s", Update.errorString()); - gIsUploadError = true; - } - /* Update was successful! */ - else - { - const uint8_t PROGRESS_FINISHED = 100U; /* % */ - - LOG_INFO("Upload of %s finished.", filename.c_str()); - - /* Filesystem is not mounted here, because we will restart in the next seconds. */ - - /* Ensure that the user see 100% update status on the display. */ - UpdateMgr::getInstance().updateProgress(PROGRESS_FINISHED); - UpdateMgr::getInstance().endProgress(); - - /* Restart is requested in upload page handler, see uploadPage(). */ + /* Finish update now. */ + if (false == Update.end(true)) + { + gIsUploadError = true; + } + /* Update was successful! */ + else + { + const uint8_t PROGRESS_FINISHED = 100U; /* % */ + + LOG_INFO("Upload of %s finished.", filename.c_str()); + + /* Filesystem is not mounted here, because we will restart in the next seconds. */ + + /* Ensure that the user see 100% update status on the display. */ + updateMgr.updateProgress(PROGRESS_FINISHED); + updateMgr.endProgress(); + updateMgr.prepareForRestart(); + + /* Restart is requested in upload page handler, see uploadPage(). */ + } } } } - else + + /* Any upload error? */ + if (true == gIsUploadError) { - /* Mount filesystem again, it may be unmounted in case of filesystem update. */ - if (false == FILESYSTEM.begin()) - { - LOG_FATAL("Couldn't mount filesystem."); - } + LOG_ERROR("Upload failed: %s", Update.errorString()); /* Abort update */ Update.abort(); - UpdateMgr::getInstance().endProgress(); + updateMgr.endProgress(); + updateMgr.prepareForRestart(); /* Inform client about abort.*/ request->send(HttpStatus::STATUS_CODE_PAYLOAD_TOO_LARGE, "text/plain", "Upload aborted.");