From f8e90efece2f533898e9363ede5d1b64d90915fd Mon Sep 17 00:00:00 2001 From: Tom Mullaney <2078150+tpmullan@users.noreply.github.com> Date: Sun, 24 May 2026 22:16:38 -0700 Subject: [PATCH] Fix detailed progress settings regressions --- octoprint_detailedprogress/__init__.py | 508 ++++++++++-------- .../static/js/DetailedProgress.js | 47 +- .../detailedprogress_settings.jinja2 | 4 +- setup.py | 50 +- tests/test_detailedprogress.py | 182 +++++++ 5 files changed, 521 insertions(+), 270 deletions(-) create mode 100644 tests/test_detailedprogress.py diff --git a/octoprint_detailedprogress/__init__.py b/octoprint_detailedprogress/__init__.py index cf37389..8a9bf37 100644 --- a/octoprint_detailedprogress/__init__.py +++ b/octoprint_detailedprogress/__init__.py @@ -10,241 +10,301 @@ from octoprint.events import Events -class DetailedProgress(octoprint.plugin.EventHandlerPlugin, - octoprint.plugin.SettingsPlugin, - octoprint.plugin.TemplatePlugin, - octoprint.plugin.AssetPlugin, - octoprint.plugin.StartupPlugin): - _last_updated = 0.0 - _last_message = 0 - _repeat_timer = None - _etl_format = "" - _eta_strftime = "" - _all_messages = [] - _messages = [] - _M73 = False - _M73_R = False - - def on_event(self, event, payload): - if event == Events.PRINT_STARTED: - self._logger.info("Printing started. Detailed progress started.") - self._etl_format = self._settings.get(["etl_format"]) - self._eta_strftime = self._settings.get(["eta_strftime"]) - self._all_messages = self._settings.get(["all_messages"]) - self._messages = self._settings.get(["messages"]) - self._M73 = self._settings.get(["use_M73"]) - self._M73_R = self._settings.get(["use_M73_R"]) - self._repeat_timer = octoprint.util.RepeatedTimer(self._settings.get_int(["time_to_change"]), self.do_work) - self._repeat_timer.start() - elif event in (Events.PRINT_DONE, Events.PRINT_FAILED, Events.PRINT_CANCELLED): - if self._repeat_timer is not None: - self._repeat_timer.cancel() - self._repeat_timer = None - self._logger.info("Printing stopped. Detailed progress stopped.") - message = self._settings.get(["print_done_message"]) - self._printer.commands("M117 {}".format(message)) - currentData = {"progress": {"completion": 100, "printTimeLeft": 0}} - self._update_progress(currentData) - elif event == Events.CONNECTED and self._settings.get(["show_ip_at_startup"]): - ip = self._get_host_ip() - if not ip: - return - self._printer.commands("M117 IP {}".format(ip)) - elif event == Events.PRINT_PAUSED: - if self._repeat_timer != None: - self._repeat_timer.cancel() - self._repeat_timer = None - self._logger.info("Printing paused. Detailed progress paused.") - elif event == Events.PRINT_RESUMED: - self._repeat_timer = octoprint.util.RepeatedTimer(self._settings.get_int(["time_to_change"]), self.do_work) - self._repeat_timer.start() - self._logger.info("Printing resumed. Detailed progress unpaused.") - - elif event.startswith('DisplayLayerProgress'): - self._layerIs = "{0}/{1}".format(payload['currentLayer'], payload['totalLayer']) - self._heightIs = "{0}/{1}".format(payload['currentHeightFormatted'], payload['totalHeightFormatted']) - self._changeFilamentSeconds = payload['changeFilamentTimeLeftInSeconds'] - - def do_work(self): - if not self._printer.is_printing(): - # we have nothing to do here - return - try: - currentData = self._printer.get_current_data() - currentData = self._sanitize_current_data(currentData) +class DetailedProgress( + octoprint.plugin.EventHandlerPlugin, + octoprint.plugin.SettingsPlugin, + octoprint.plugin.TemplatePlugin, + octoprint.plugin.AssetPlugin, + octoprint.plugin.StartupPlugin, +): + _last_updated = 0.0 + _last_message = 0 + _repeat_timer = None + _etl_format = "" + _eta_strftime = "" + _all_messages = [] + _messages = [] + _M73 = False + _M73_R = False - message = self._get_next_message(currentData) - self._logger.info("Message: {0}".format(message)) - - self._printer.commands("M117 {}".format(message)) - if self._M73: - self._update_progress(currentData) + def on_event(self, event, payload): + if event == Events.PRINT_STARTED: + self._logger.info("Printing started. Detailed progress started.") + self._refresh_settings() + self._repeat_timer = octoprint.util.RepeatedTimer( + self._settings.get_int(["time_to_change"]), self.do_work + ) + self._repeat_timer.start() + elif event in (Events.PRINT_DONE, Events.PRINT_FAILED, Events.PRINT_CANCELLED): + if self._repeat_timer is not None: + self._repeat_timer.cancel() + self._repeat_timer = None + self._logger.info("Printing stopped. Detailed progress stopped.") + self._refresh_settings() + message = self._get_print_done_message(payload) + self._printer.commands("M117 {}".format(message)) + currentData = {"progress": {"completion": 100, "printTimeLeft": 0}} + if self._M73: + self._update_progress(currentData) + elif event == Events.CONNECTED and self._settings.get(["show_ip_at_startup"]): + ip = self._get_host_ip() + if not ip: + return + self._printer.commands("M117 IP {}".format(ip)) + elif event == Events.PRINT_PAUSED: + if self._repeat_timer is not None: + self._repeat_timer.cancel() + self._repeat_timer = None + self._logger.info("Printing paused. Detailed progress paused.") + elif event == Events.PRINT_RESUMED: + self._repeat_timer = octoprint.util.RepeatedTimer( + self._settings.get_int(["time_to_change"]), self.do_work + ) + self._repeat_timer.start() + self._logger.info("Printing resumed. Detailed progress unpaused.") - except Exception as e: - self._logger.info("Caught an exception {0}\nTraceback:{1}".format(e, traceback.format_exc())) + elif event.startswith("DisplayLayerProgress"): + self._layerIs = "{0}/{1}".format( + payload["currentLayer"], payload["totalLayer"] + ) + self._heightIs = "{0}/{1}".format( + payload["currentHeightFormatted"], payload["totalHeightFormatted"] + ) + self._changeFilamentSeconds = payload["changeFilamentTimeLeftInSeconds"] - def _update_progress(self, currentData): - progressPerc = int(currentData["progress"]["completion"]) - if self._M73_R: - try: - printMinutesLeft = int(currentData["progress"]["printTimeLeft"] / 60) - self._printer.commands("M73 P{} R{}".format(progressPerc, printMinutesLeft)) - except TypeError: - self._printer.commands("M73 P{}".format(progressPerc)) - else: - self._printer.commands("M73 P{}".format(progressPerc)) + def do_work(self): + if not self._printer.is_printing(): + # we have nothing to do here + return + try: + self._refresh_settings() + currentData = self._printer.get_current_data() + currentData = self._sanitize_current_data(currentData) - def _sanitize_current_data(self, currentData): - if currentData["progress"]["printTimeLeft"] is None: - currentData["progress"]["printTimeLeft"] = currentData["job"]["estimatedPrintTime"] - if currentData["progress"]["filepos"] is None: - currentData["progress"]["filepos"] = 0 - if currentData["progress"]["printTime"] is None: - currentData["progress"]["printTime"] = currentData["job"]["estimatedPrintTime"] + message = self._get_next_message(currentData) + self._logger.info("Message: {0}".format(message)) - currentData["progress"]["printTimeLeftString"] = "No ETL yet" - currentData["progress"]["ETA"] = "No ETA yet" - currentData["progress"]["layerProgress"] = "N/A" - currentData["progress"]["heightProgress"] = "N/A" - currentData["progress"]["changeFilamentIn"] = "N/A" - - accuracy = currentData["progress"]["printTimeLeftOrigin"] - if accuracy: - if accuracy == "estimate": - accuracy = "Best" - elif accuracy == "average" or accuracy == "genius": - accuracy = "Good" - elif accuracy == "analysis" or accuracy.startswith("mixed"): - accuracy = "Medium" - elif accuracy == "linear": - accuracy = "Bad" - else: - accuracy = "ERR" - self._logger.debug("Caught unmapped accuracy value: {0}".format(accuracy)) - else: - accuracy = "N/A" - currentData["progress"]["accuracy"] = accuracy - - currentData["progress"]["filename"] = currentData["job"]["file"]["name"] + self._printer.commands("M117 {}".format(message)) + if self._M73: + self._update_progress(currentData) - # Add additional data - try: - currentData["progress"]["printTimeString"] = self._get_time_from_seconds( - currentData["progress"]["printTime"]) - currentData["progress"]["printTimeLeftString"] = self._get_time_from_seconds( - currentData["progress"]["printTimeLeft"]) - currentData["progress"]["ETA"] = time.strftime(self._eta_strftime, time.localtime( - time.time() + currentData["progress"]["printTimeLeft"])) - currentData["progress"]["layerProgress"] = self._layerIs - currentData["progress"]["heightProgress"] = self._heightIs - if isinstance(self._changeFilamentSeconds, int): - if self._changeFilamentSeconds == 0: - currentData["progress"]["changeFilamentIn"] = "N/A" - else: - currentData["progress"]["changeFilamentIn"] = self._get_time_from_seconds( - self._changeFilamentSeconds) - except Exception as e: - self._logger.debug( - "Caught an exception trying to parse data: {0}\n Error is: {1}\nTraceback:{2}".format(currentData, e, - traceback.format_exc())) + except Exception as e: + self._logger.info( + "Caught an exception {0}\nTraceback:{1}".format( + e, traceback.format_exc() + ) + ) - return currentData + def _update_progress(self, currentData): + progressPerc = int(currentData["progress"]["completion"]) + if self._M73_R: + try: + printMinutesLeft = int(currentData["progress"]["printTimeLeft"] / 60) + self._printer.commands( + "M73 P{} R{}".format(progressPerc, printMinutesLeft) + ) + except TypeError: + self._printer.commands("M73 P{}".format(progressPerc)) + else: + self._printer.commands("M73 P{}".format(progressPerc)) - def _get_next_message(self, currentData): - message = self._messages[self._last_message] - self._last_message += 1 - if self._last_message >= len(self._messages): - self._last_message = 0 - return message.format( - completion=currentData["progress"]["completion"], - printTime=currentData["progress"]["printTimeString"], - printTimeLeft=currentData["progress"]["printTimeLeftString"], - ETA=currentData["progress"]["ETA"], - filepos=currentData["progress"]["filepos"], - accuracy=currentData["progress"]["accuracy"], - filename=currentData["progress"]["filename"], - layerProgress=currentData["progress"]["layerProgress"], - heightProgress=currentData["progress"]["heightProgress"], - changeFilamentIn = currentData["progress"]["changeFilamentIn"] - ) + def _refresh_settings(self): + self._etl_format = self._settings.get(["etl_format"]) + self._eta_strftime = self._settings.get(["eta_strftime"]) + self._all_messages = self._settings.get(["all_messages"]) + self._messages = self._settings.get(["messages"]) + self._M73 = self._settings.get(["use_M73"]) + self._M73_R = self._settings.get(["use_M73_R"]) - def _get_time_from_seconds(self, seconds): - hours = 0 - minutes = 0 - if seconds >= 3600: - hours = int(seconds / 3600) - seconds = seconds % 3600 - if seconds >= 60: - minutes = int(seconds / 60) - seconds = seconds % 60 - return self._etl_format.format(**locals()) + def _get_print_done_message(self, payload): + message = self._settings.get(["print_done_message"]) + printTime = None + if payload: + printTime = payload.get("time") + if printTime is None: + try: + printTime = self._printer.get_current_data()["progress"]["printTime"] + except Exception: + printTime = 0 + printTimeString = self._get_time_from_seconds(printTime) + return message.format(printTime=printTimeString) - def _get_host_ip(self): - host_ip = os.environ.get("HOST_IP") - if host_ip: - return host_ip - return [l for l in ( - [ip for ip in socket.gethostbyname_ex(socket.gethostname())[2] if not ip.startswith("127.")][:1], [ - [(s.connect(('8.8.8.8', 53)), s.getsockname()[0], s.close()) for s in - [socket.socket(socket.AF_INET, socket.SOCK_DGRAM)]][0][1]]) if l][0][0] + def _sanitize_current_data(self, currentData): + if currentData["progress"]["printTimeLeft"] is None: + currentData["progress"]["printTimeLeft"] = currentData["job"][ + "estimatedPrintTime" + ] + if currentData["progress"]["filepos"] is None: + currentData["progress"]["filepos"] = 0 + if currentData["progress"]["printTime"] is None: + currentData["progress"]["printTime"] = currentData["job"][ + "estimatedPrintTime" + ] - ##~~ StartupPlugin - def on_after_startup(self): - self._logger.info("OctoPrint-DetailedProgress loaded!") + currentData["progress"]["printTimeLeftString"] = "No ETL yet" + currentData["progress"]["ETA"] = "No ETA yet" + currentData["progress"]["layerProgress"] = "N/A" + currentData["progress"]["heightProgress"] = "N/A" + currentData["progress"]["changeFilamentIn"] = "N/A" - ##-- AssetPlugin - def get_assets(self): - return dict(js=["js/DetailedProgress.js"], css=["css/detailedprogress.css"]) + accuracy = currentData["progress"]["printTimeLeftOrigin"] + if accuracy: + if accuracy == "estimate": + accuracy = "Best" + elif accuracy == "average" or accuracy == "genius": + accuracy = "Good" + elif accuracy == "analysis" or accuracy.startswith("mixed"): + accuracy = "Medium" + elif accuracy == "linear": + accuracy = "Bad" + else: + accuracy = "ERR" + self._logger.debug( + "Caught unmapped accuracy value: {0}".format(accuracy) + ) + else: + accuracy = "N/A" + currentData["progress"]["accuracy"] = accuracy - ##~~ Settings - def get_settings_defaults(self): - return dict( - time_to_change="10", - eta_strftime="%-m/%d %-I.%M%p", - etl_format="{hours:02d}h{minutes:02d}m{seconds:02d}s", - print_done_message="Print Done", - use_M73=True, - use_M73_R=False, - show_ip_at_startup=True, - all_messages=[ - '{filename}', - '{completion:.2f}% complete', - 'ETL {printTimeLeft}', - 'ETA {ETA}', - '{accuracy} accuracy', - 'Layer {layerProgress}', - 'Height {heightProgress}', - 'Fil. change {changeFilamentIn}' - ], - messages=[ - '{completion:.2f}% complete', - 'ETL {printTimeLeft}', - 'ETA {ETA}', - '{accuracy} accuracy' - ] - ) + currentData["progress"]["filename"] = currentData["job"]["file"]["name"] - ##-- Template hooks - def get_template_configs(self): - return [dict(type="settings", custom_bindings=False)] + # Add additional data + try: + currentData["progress"]["printTimeString"] = self._get_time_from_seconds( + currentData["progress"]["printTime"] + ) + currentData["progress"]["printTimeLeftString"] = ( + self._get_time_from_seconds(currentData["progress"]["printTimeLeft"]) + ) + currentData["progress"]["ETA"] = time.strftime( + self._eta_strftime, + time.localtime(time.time() + currentData["progress"]["printTimeLeft"]), + ) + currentData["progress"]["layerProgress"] = self._layerIs + currentData["progress"]["heightProgress"] = self._heightIs + if isinstance(self._changeFilamentSeconds, int): + if self._changeFilamentSeconds == 0: + currentData["progress"]["changeFilamentIn"] = "N/A" + else: + currentData["progress"]["changeFilamentIn"] = ( + self._get_time_from_seconds(self._changeFilamentSeconds) + ) + except Exception as e: + self._logger.debug( + "Caught an exception trying to parse data: {0}\n Error is: {1}\nTraceback:{2}".format( + currentData, e, traceback.format_exc() + ) + ) - ##~~ Softwareupdate hook - def get_update_information(self): - return dict( - detailedprogress=dict( - displayName="DetailedProgress Plugin", - displayVersion=self._plugin_version, + return currentData - # version check: github repository - type="github_release", - user="tpmullan", - repo="OctoPrint-DetailedProgress", - current=self._plugin_version, + def _get_next_message(self, currentData): + message = self._messages[self._last_message] + self._last_message += 1 + if self._last_message >= len(self._messages): + self._last_message = 0 + return message.format( + completion=currentData["progress"]["completion"], + printTime=currentData["progress"]["printTimeString"], + printTimeLeft=currentData["progress"]["printTimeLeftString"], + ETA=currentData["progress"]["ETA"], + filepos=currentData["progress"]["filepos"], + accuracy=currentData["progress"]["accuracy"], + filename=currentData["progress"]["filename"], + layerProgress=currentData["progress"]["layerProgress"], + heightProgress=currentData["progress"]["heightProgress"], + changeFilamentIn=currentData["progress"]["changeFilamentIn"], + ) - # update method: pip - pip="https://github.com/tpmullan/OctoPrint-DetailedProgress/archive/{target_version}.zip" - ) - ) + def _get_time_from_seconds(self, seconds): + seconds = int(seconds) + hours = 0 + minutes = 0 + if seconds >= 3600: + hours = int(seconds / 3600) + seconds = seconds % 3600 + if seconds >= 60: + minutes = int(seconds / 60) + seconds = seconds % 60 + return self._etl_format.format(**locals()) + + def _get_host_ip(self): + host_ip = os.environ.get("HOST_IP") + if host_ip: + return host_ip + return [ + address + for address in ( + [ + ip + for ip in socket.gethostbyname_ex(socket.gethostname())[2] + if not ip.startswith("127.") + ][:1], + [ + [ + (s.connect(("8.8.8.8", 53)), s.getsockname()[0], s.close()) + for s in [socket.socket(socket.AF_INET, socket.SOCK_DGRAM)] + ][0][1] + ], + ) + if address + ][0][0] + + ##~~ StartupPlugin + def on_after_startup(self): + self._logger.info("OctoPrint-DetailedProgress loaded!") + + ##-- AssetPlugin + def get_assets(self): + return dict(js=["js/DetailedProgress.js"], css=["css/detailedprogress.css"]) + + ##~~ Settings + def get_settings_defaults(self): + return dict( + time_to_change="10", + eta_strftime="%-m/%d %-I.%M%p", + etl_format="{hours:02d}h{minutes:02d}m{seconds:02d}s", + print_done_message="Print Done", + use_M73=True, + use_M73_R=False, + show_ip_at_startup=True, + all_messages=[ + "{filename}", + "{completion:.2f}% complete", + "ETL {printTimeLeft}", + "ETA {ETA}", + "{accuracy} accuracy", + "Layer {layerProgress}", + "Height {heightProgress}", + "Fil. change {changeFilamentIn}", + ], + messages=[ + "{completion:.2f}% complete", + "ETL {printTimeLeft}", + "ETA {ETA}", + "{accuracy} accuracy", + ], + ) + + ##-- Template hooks + def get_template_configs(self): + return [dict(type="settings", custom_bindings=False)] + + ##~~ Softwareupdate hook + def get_update_information(self): + return dict( + detailedprogress=dict( + displayName="DetailedProgress Plugin", + displayVersion=self._plugin_version, + # version check: github repository + type="github_release", + user="tpmullan", + repo="OctoPrint-DetailedProgress", + current=self._plugin_version, + # update method: pip + pip="https://github.com/tpmullan/OctoPrint-DetailedProgress/archive/{target_version}.zip", + ) + ) __plugin_name__ = "Detailed Progress" @@ -252,10 +312,10 @@ def get_update_information(self): def __plugin_load__(): - global __plugin_implementation__ - __plugin_implementation__ = DetailedProgress() + global __plugin_implementation__ + __plugin_implementation__ = DetailedProgress() - global __plugin_hooks__ - __plugin_hooks__ = { - "octoprint.plugin.softwareupdate.check_config": __plugin_implementation__.get_update_information, - } + global __plugin_hooks__ + __plugin_hooks__ = { + "octoprint.plugin.softwareupdate.check_config": __plugin_implementation__.get_update_information, + } diff --git a/octoprint_detailedprogress/static/js/DetailedProgress.js b/octoprint_detailedprogress/static/js/DetailedProgress.js index b660da1..ee2890c 100644 --- a/octoprint_detailedprogress/static/js/DetailedProgress.js +++ b/octoprint_detailedprogress/static/js/DetailedProgress.js @@ -34,36 +34,41 @@ $(function() { }; self.onEventSettingsUpdated = function (payload) { - self.time_to_change = self.settings.settings.plugins.detailedprogress.time_to_change(); - self.eta_strftime = self.settings.settings.plugins.detailedprogress.eta_strftime(); - self.etl_format = self.settings.settings.plugins.detailedprogress.etl_format(); - self.use_M73 = self.settings.settings.plugins.detailedprogress.use_M73(); - self.use_M73_R = self.settings.settings.plugins.detailedprogress.use_M73_R(); - self.show_ip_at_startup = self.settings.settings.plugins.detailedprogress.show_ip_at_startup(); - self.print_done_message = self.settings.settings.plugins.detailedprogress.print_done_message(); - self.messages = self.settings.settings.plugins.detailedprogress.messages(); - self.all_messages = self.settings.settings.plugins.detailedprogress.all_messages(); + self.time_to_change(self.settings.settings.plugins.detailedprogress.time_to_change()); + self.eta_strftime(self.settings.settings.plugins.detailedprogress.eta_strftime()); + self.etl_format(self.settings.settings.plugins.detailedprogress.etl_format()); + self.use_M73(self.settings.settings.plugins.detailedprogress.use_M73()); + self.use_M73_R(self.settings.settings.plugins.detailedprogress.use_M73_R()); + self.show_ip_at_startup(self.settings.settings.plugins.detailedprogress.show_ip_at_startup()); + self.print_done_message(self.settings.settings.plugins.detailedprogress.print_done_message()); + self.messages(self.settings.settings.plugins.detailedprogress.messages()); + self.all_messages(self.settings.settings.plugins.detailedprogress.all_messages()); }; $('#dtPrg_addMsg').click(function () { - var msgToAdd = $('#dtPrg_msgToAdd').val(); + var msgToAdd = $.trim($('#dtPrg_msgToAdd').val()); + var allMessages = self.settings.settings.plugins.detailedprogress.all_messages; + var activeMessages = self.settings.settings.plugins.detailedprogress.messages; // Prevent blanks and duplicates if (msgToAdd.length > 0 && - $('#dtPrg_selMsg option').filter(function () { return $(this).html() == msgToAdd; }).length == 0) { - $('#dtPrg_selMsg').append(''); - self.all_messages.push(msgToAdd); - } - $('#dtPrg_msgToAdd').val('');; // Clear the text box - return false; + ko.utils.arrayIndexOf(allMessages(), msgToAdd) == -1) { + allMessages.push(msgToAdd); + } + if (msgToAdd.length > 0 && + ko.utils.arrayIndexOf(activeMessages(), msgToAdd) == -1) { + activeMessages.push(msgToAdd); + } + $('#dtPrg_msgToAdd').val(''); // Clear the text box + return false; }); $('#dtPrg_removeSelected').click(function () { - var msgToRemove = $('#dtPrg_selMsg').val(); + var msgToRemove = $('#dtPrg_selMsg').val() || []; + var allMessages = self.settings.settings.plugins.detailedprogress.all_messages; + var activeMessages = self.settings.settings.plugins.detailedprogress.messages; if (msgToRemove.length == 0) return false; - $('#dtPrg_selMsg option').each(function() { - if (msgToRemove.indexOf($(this).val()) > - 1) $(this).remove(); - }); - self.all_messages.removeAll(msgToRemove); + allMessages.removeAll(msgToRemove); + activeMessages.removeAll(msgToRemove); return false; }); }; diff --git a/octoprint_detailedprogress/templates/detailedprogress_settings.jinja2 b/octoprint_detailedprogress/templates/detailedprogress_settings.jinja2 index f8290c8..5a4d66f 100644 --- a/octoprint_detailedprogress/templates/detailedprogress_settings.jinja2 +++ b/octoprint_detailedprogress/templates/detailedprogress_settings.jinja2 @@ -4,7 +4,7 @@