From 5fba1fd8ec95ad789ef95102ff7e589e4cc230df Mon Sep 17 00:00:00 2001 From: Jacopo Tediosi Date: Tue, 5 May 2026 01:08:21 +0200 Subject: [PATCH 1/6] Format with Ruff --- octoprint_automaticshutdown/__init__.py | 443 +++++++++++++----------- 1 file changed, 241 insertions(+), 202 deletions(-) diff --git a/octoprint_automaticshutdown/__init__.py b/octoprint_automaticshutdown/__init__.py index b89e94b..1293bd3 100644 --- a/octoprint_automaticshutdown/__init__.py +++ b/octoprint_automaticshutdown/__init__.py @@ -1,215 +1,254 @@ # coding=utf-8 from __future__ import absolute_import +import time + import octoprint.plugin -from octoprint.server import user_permission -from octoprint.util import RepeatedTimer -from octoprint.events import eventManager, Events import octoprint.timelapse from flask import make_response -import time +from octoprint.events import Events, eventManager +from octoprint.server import user_permission +from octoprint.util import RepeatedTimer + + +class AutomaticshutdownPlugin( + octoprint.plugin.TemplatePlugin, + octoprint.plugin.AssetPlugin, + octoprint.plugin.SimpleApiPlugin, + octoprint.plugin.EventHandlerPlugin, + octoprint.plugin.SettingsPlugin, + octoprint.plugin.StartupPlugin, +): + def __init__(self): + self.abortTimeout = 0 + self.rememberCheckBox = False + self.lastCheckBoxValue = False + self._automatic_shutdown_enabled = False + self._timeout_value = None + self._abort_timer = None + self._wait_for_timelapse_timer = None + + def initialize(self): + self.abortTimeout = self._settings.get_int(["abortTimeout"]) + self._logger.debug("abortTimeout: %s" % self.abortTimeout) + + self.rememberCheckBox = self._settings.get_boolean(["rememberCheckBox"]) + self._logger.debug("rememberCheckBox: %s" % self.rememberCheckBox) + + self.lastCheckBoxValue = self._settings.get_boolean(["lastCheckBoxValue"]) + self._logger.debug("lastCheckBoxValue: %s" % self.lastCheckBoxValue) + if self.rememberCheckBox: + self._automatic_shutdown_enabled = self.lastCheckBoxValue + + def get_assets(self): + return dict(js=["js/automaticshutdown.js"]) + + def get_template_configs(self): + return [ + dict( + type="sidebar", + name="Automatic Shutdown", + custom_bindings=False, + icon="power-off", + ), + dict(type="settings", custom_bindings=False), + ] + + def get_api_commands(self): + return dict(enable=[], disable=[], abort=[]) + + def on_api_command(self, command, data): + if not user_permission.can(): + return make_response("Insufficient rights", 403) + + if command == "enable": + self._automatic_shutdown_enabled = True + self._logger.info("Automatic shutdown enabled") + elif command == "disable": + self._automatic_shutdown_enabled = False + self._logger.info("Automatic shutdown disabled") + elif command == "abort": + if self._wait_for_timelapse_timer is not None: + self._wait_for_timelapse_timer.cancel() + self._wait_for_timelapse_timer = None + if self._abort_timer is not None: + self._abort_timer.cancel() + self._abort_timer = None + self._timeout_value = None + self._logger.info("Shutdown aborted") + + if command == "enable" or command == "disable": + self.lastCheckBoxValue = self._automatic_shutdown_enabled + if self.rememberCheckBox: + self._settings.set_boolean( + ["lastCheckBoxValue"], self.lastCheckBoxValue + ) + self._settings.save() + eventManager().fire(Events.SETTINGS_UPDATED) + + self._plugin_manager.send_plugin_message( + self._identifier, + dict( + automaticShutdownEnabled=self._automatic_shutdown_enabled, + type="timeout", + timeout_value=self._timeout_value, + ), + ) + + def on_event(self, event, payload): + if event == Events.CLIENT_OPENED: + self._plugin_manager.send_plugin_message( + self._identifier, + dict( + automaticShutdownEnabled=self._automatic_shutdown_enabled, + type="timeout", + timeout_value=self._timeout_value, + ), + ) + return + + if not self._automatic_shutdown_enabled: + return + + if not self._settings.global_get( + ["server", "commands", "systemShutdownCommand"] + ): + self._logger.warning( + "systemShutdownCommand is not defined. Aborting shutdown..." + ) + return + + if event not in [Events.PRINT_DONE, Events.PRINT_FAILED]: + return + + if event == Events.PRINT_FAILED and not self._printer.is_closed_or_error(): + # Cancelled job + return + + if event in [Events.PRINT_DONE, Events.PRINT_FAILED]: + self._logger.info("Starting abort shutdown timer.") + + webcam_config = self._settings.global_get( + ["webcam", "timelapse"], merged=True + ) + timelapse_type = webcam_config["type"] + if timelapse_type is not None and timelapse_type != "off": + self._wait_for_timelapse_start() + else: + self._timer_start() + return + + def _wait_for_timelapse_start(self): + if self._wait_for_timelapse_timer is not None: + return + + self._wait_for_timelapse_timer = RepeatedTimer(5, self._wait_for_timelapse) + self._wait_for_timelapse_timer.start() + + def _wait_for_timelapse(self): + c = len(octoprint.timelapse.get_unrendered_timelapses()) + + if c > 0: + self._logger.info( + "Waiting for %s timelapse(s) to finish rendering before starting shutdown timer..." + % c + ) + self._plugin_manager.send_plugin_message( + self._identifier, + dict( + automaticShutdownEnabled=self._automatic_shutdown_enabled, + type="timelapse_processing", + ), + ) + else: + self._timer_start() + + def _timer_start(self): + if self._abort_timer is not None: + return + + if self._wait_for_timelapse_timer is not None: + self._wait_for_timelapse_timer.cancel() + + self._timeout_value = self.abortTimeout + self._abort_timer = RepeatedTimer(1, self._timer_task) + self._abort_timer.start() + + def _timer_task(self): + if self._timeout_value is None: + return + + self._timeout_value -= 1 + self._plugin_manager.send_plugin_message( + self._identifier, + dict( + automaticShutdownEnabled=self._automatic_shutdown_enabled, + type="timeout", + timeout_value=self._timeout_value, + ), + ) + self._logger.info("Shutting down in {} seconds".format(self._timeout_value)) + if self._timeout_value <= 0: + if self._wait_for_timelapse_timer is not None: + self._wait_for_timelapse_timer.cancel() + self._wait_for_timelapse_timer = None + if self._abort_timer is not None: + self._abort_timer.cancel() + self._abort_timer = None + self._shutdown_system() + + def _shutdown_system(self): + shutdown_command = self._settings.global_get( + ["server", "commands", "systemShutdownCommand"] + ) + self._logger.info( + "Shutting down system with command: {command}".format( + command=shutdown_command + ) + ) + try: + import sarge + + p = sarge.run(shutdown_command, async_=True) + except Exception as e: + self._logger.exception("Error when shutting down: {error}".format(error=e)) + return + + def get_settings_defaults(self): + return dict(abortTimeout=30, rememberCheckBox=False, lastCheckBoxValue=False) + + def on_settings_save(self, data): + octoprint.plugin.SettingsPlugin.on_settings_save(self, data) + + self.abortTimeout = self._settings.get_int(["abortTimeout"]) + self.rememberCheckBox = self._settings.get_boolean(["rememberCheckBox"]) + self.lastCheckBoxValue = self._settings.get_boolean(["lastCheckBoxValue"]) + + def get_update_information(self): + return dict( + automaticshutdown=dict( + displayName="Automatic Shutdown", + displayVersion=self._plugin_version, + # version check: github repository + type="github_release", + user="OctoPrint", + repo="OctoPrint-AutomaticShutdown", + current=self._plugin_version, + # update method: pip w/ dependency links + pip="https://github.com/OctoPrint/OctoPrint-AutomaticShutdown/archive/{target_version}.zip", + ) + ) -class AutomaticshutdownPlugin(octoprint.plugin.TemplatePlugin, - octoprint.plugin.AssetPlugin, - octoprint.plugin.SimpleApiPlugin, - octoprint.plugin.EventHandlerPlugin, - octoprint.plugin.SettingsPlugin, - octoprint.plugin.StartupPlugin): - - def __init__(self): - self.abortTimeout = 0 - self.rememberCheckBox = False - self.lastCheckBoxValue = False - self._automatic_shutdown_enabled = False - self._timeout_value = None - self._abort_timer = None - self._wait_for_timelapse_timer = None - - def initialize(self): - self.abortTimeout = self._settings.get_int(["abortTimeout"]) - self._logger.debug("abortTimeout: %s" % self.abortTimeout) - - self.rememberCheckBox = self._settings.get_boolean(["rememberCheckBox"]) - self._logger.debug("rememberCheckBox: %s" % self.rememberCheckBox) - - self.lastCheckBoxValue = self._settings.get_boolean(["lastCheckBoxValue"]) - self._logger.debug("lastCheckBoxValue: %s" % self.lastCheckBoxValue) - if self.rememberCheckBox: - self._automatic_shutdown_enabled = self.lastCheckBoxValue - - def get_assets(self): - return dict(js=["js/automaticshutdown.js"]) - - def get_template_configs(self): - return [dict( - type="sidebar", - name="Automatic Shutdown", - custom_bindings=False, - icon="power-off" - ),dict( - type="settings", - custom_bindings=False - )] - - - def get_api_commands(self): - return dict( - enable=[], - disable=[], - abort=[] - ) - - def on_api_command(self, command, data): - if not user_permission.can(): - return make_response("Insufficient rights", 403) - - if command == "enable": - self._automatic_shutdown_enabled = True - self._logger.info("Automatic shutdown enabled") - elif command == "disable": - self._automatic_shutdown_enabled = False - self._logger.info("Automatic shutdown disabled") - elif command == "abort": - if self._wait_for_timelapse_timer is not None: - self._wait_for_timelapse_timer.cancel() - self._wait_for_timelapse_timer = None - if self._abort_timer is not None: - self._abort_timer.cancel() - self._abort_timer = None - self._timeout_value = None - self._logger.info("Shutdown aborted") - - if command == "enable" or command == "disable": - self.lastCheckBoxValue = self._automatic_shutdown_enabled - if self.rememberCheckBox: - self._settings.set_boolean(["lastCheckBoxValue"], self.lastCheckBoxValue) - self._settings.save() - eventManager().fire(Events.SETTINGS_UPDATED) - - self._plugin_manager.send_plugin_message(self._identifier, dict(automaticShutdownEnabled=self._automatic_shutdown_enabled, type="timeout", timeout_value=self._timeout_value)) - - def on_event(self, event, payload): - if event == Events.CLIENT_OPENED: - self._plugin_manager.send_plugin_message(self._identifier, dict(automaticShutdownEnabled=self._automatic_shutdown_enabled, type="timeout", timeout_value=self._timeout_value)) - return - - if not self._automatic_shutdown_enabled: - return - - if not self._settings.global_get(["server", "commands", "systemShutdownCommand"]): - self._logger.warning("systemShutdownCommand is not defined. Aborting shutdown...") - return - - if event not in [Events.PRINT_DONE, Events.PRINT_FAILED]: - return - - if event == Events.PRINT_FAILED and not self._printer.is_closed_or_error(): - #Cancelled job - return - - if event in [Events.PRINT_DONE, Events.PRINT_FAILED]: - self._logger.info("Starting abort shutdown timer.") - - webcam_config = self._settings.global_get(["webcam", "timelapse"], merged=True) - timelapse_type = webcam_config["type"] - if (timelapse_type is not None and timelapse_type != "off"): - self._wait_for_timelapse_start() - else: - self._timer_start() - return - - def _wait_for_timelapse_start(self): - if self._wait_for_timelapse_timer is not None: - return - - self._wait_for_timelapse_timer = RepeatedTimer(5, self._wait_for_timelapse) - self._wait_for_timelapse_timer.start() - - def _wait_for_timelapse(self): - c = len(octoprint.timelapse.get_unrendered_timelapses()) - - if c > 0: - self._logger.info("Waiting for %s timelapse(s) to finish rendering before starting shutdown timer..." % c) - self._plugin_manager.send_plugin_message(self._identifier, dict(automaticShutdownEnabled=self._automatic_shutdown_enabled, type="timelapse_processing")) - else: - self._timer_start() - - def _timer_start(self): - if self._abort_timer is not None: - return - - if self._wait_for_timelapse_timer is not None: - self._wait_for_timelapse_timer.cancel() - - self._timeout_value = self.abortTimeout - self._abort_timer = RepeatedTimer(1, self._timer_task) - self._abort_timer.start() - - def _timer_task(self): - if self._timeout_value is None: - return - - self._timeout_value -= 1 - self._plugin_manager.send_plugin_message(self._identifier, dict(automaticShutdownEnabled=self._automatic_shutdown_enabled, type="timeout", timeout_value=self._timeout_value)) - self._logger.info("Shutting down in {} seconds".format(self._timeout_value)) - if self._timeout_value <= 0: - if self._wait_for_timelapse_timer is not None: - self._wait_for_timelapse_timer.cancel() - self._wait_for_timelapse_timer = None - if self._abort_timer is not None: - self._abort_timer.cancel() - self._abort_timer = None - self._shutdown_system() - - def _shutdown_system(self): - shutdown_command = self._settings.global_get(["server", "commands", "systemShutdownCommand"]) - self._logger.info("Shutting down system with command: {command}".format(command=shutdown_command)) - try: - import sarge - p = sarge.run(shutdown_command, async_=True) - except Exception as e: - self._logger.exception("Error when shutting down: {error}".format(error=e)) - return - - def get_settings_defaults(self): - return dict( - abortTimeout = 30, - rememberCheckBox = False, - lastCheckBoxValue = False - ) - - def on_settings_save(self, data): - octoprint.plugin.SettingsPlugin.on_settings_save(self, data) - - self.abortTimeout = self._settings.get_int(["abortTimeout"]) - self.rememberCheckBox = self._settings.get_boolean(["rememberCheckBox"]) - self.lastCheckBoxValue = self._settings.get_boolean(["lastCheckBoxValue"]) - - def get_update_information(self): - return dict( - automaticshutdown=dict( - displayName="Automatic Shutdown", - displayVersion=self._plugin_version, - - # version check: github repository - type="github_release", - user="OctoPrint", - repo="OctoPrint-AutomaticShutdown", - current=self._plugin_version, - - # update method: pip w/ dependency links - pip="https://github.com/OctoPrint/OctoPrint-AutomaticShutdown/archive/{target_version}.zip" - ) - ) __plugin_name__ = "Automatic Shutdown" __plugin_pythoncompat__ = ">=2.7,<4" + def __plugin_load__(): - global __plugin_implementation__ - __plugin_implementation__ = AutomaticshutdownPlugin() + global __plugin_implementation__ + __plugin_implementation__ = AutomaticshutdownPlugin() - 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 + } From ad63255205d812ded9729a7c3fe1c2a43d53f05c Mon Sep 17 00:00:00 2001 From: Jacopo Tediosi Date: Tue, 5 May 2026 01:37:10 +0200 Subject: [PATCH 2/6] Implement custom permission `user_permission` has been removed in OctoPrint 2.0.0. This commit replaces it by implementing a custom permission as suggested by the documentation: https://docs.octoprint.org/en/dev/plugins/migration_2_0_0.html#removed-octoprint-users --- octoprint_automaticshutdown/__init__.py | 33 +++++++++- .../static/js/automaticshutdown.js | 66 +++++++++++-------- .../automaticshutdown_sidebar.jinja2 | 3 +- 3 files changed, 70 insertions(+), 32 deletions(-) diff --git a/octoprint_automaticshutdown/__init__.py b/octoprint_automaticshutdown/__init__.py index 1293bd3..1f1063d 100644 --- a/octoprint_automaticshutdown/__init__.py +++ b/octoprint_automaticshutdown/__init__.py @@ -6,8 +6,9 @@ import octoprint.plugin import octoprint.timelapse from flask import make_response +from flask_babel import gettext +from octoprint.access.permissions import ADMIN_GROUP, USER_GROUP, Permissions from octoprint.events import Events, eventManager -from octoprint.server import user_permission from octoprint.util import RepeatedTimer @@ -58,7 +59,15 @@ def get_api_commands(self): return dict(enable=[], disable=[], abort=[]) def on_api_command(self, command, data): - if not user_permission.can(): + required_permissions = { + "enable": Permissions.PLUGIN_AUTOMATICSHUTDOWN_ENABLE, + "disable": Permissions.PLUGIN_AUTOMATICSHUTDOWN_ENABLE, + "abort": Permissions.PLUGIN_AUTOMATICSHUTDOWN_ABORT, + } + permission = required_permissions.get(command) + if permission is None: + return make_response("Unknown command", 400) + if not permission.can(): return make_response("Insufficient rights", 403) if command == "enable": @@ -239,6 +248,25 @@ def get_update_information(self): ) ) + def get_additional_permissions(self, *args, **kwargs): + return [ + dict( + key="ENABLE", + name="Enable Automatic Shutdown", + description=gettext("Allows to enable/disable Automatic Shutdown"), + roles=["user"], + dangerous=False, + default_groups=[ADMIN_GROUP, USER_GROUP], + ), + dict( + key="ABORT", + name="Abort Automatic Shutdown", + description=gettext("Allows to abort a pending Automatic Shutdown"), + roles=["user"], + dangerous=False, + default_groups=[ADMIN_GROUP, USER_GROUP], + ), + ] __plugin_name__ = "Automatic Shutdown" __plugin_pythoncompat__ = ">=2.7,<4" @@ -250,5 +278,6 @@ def __plugin_load__(): global __plugin_hooks__ __plugin_hooks__ = { + "octoprint.access.permissions": __plugin_implementation__.get_additional_permissions, "octoprint.plugin.softwareupdate.check_config": __plugin_implementation__.get_update_information } diff --git a/octoprint_automaticshutdown/static/js/automaticshutdown.js b/octoprint_automaticshutdown/static/js/automaticshutdown.js index 61963f7..d29f3c5 100644 --- a/octoprint_automaticshutdown/static/js/automaticshutdown.js +++ b/octoprint_automaticshutdown/static/js/automaticshutdown.js @@ -3,6 +3,7 @@ $(function() { var self = this; self.loginState = parameters[0]; + self.access = parameters[1]; self.automaticShutdownEnabled = ko.observable(); // Hack to remove automatically added Cancel button @@ -10,22 +11,25 @@ $(function() { PNotify.prototype.options.confirm.buttons = []; self.timeoutPopupShutdownText = gettext('Shutting down in '); self.timeoutPopupTimelapseProcessingText = gettext('Waiting for timelapse to finish processing... '); + self.abortShutdownButton = { + text: 'Abort Shutdown', + addClass: 'btn-block btn-danger', + promptTrigger: true, + click: function(notice, value){ + notice.remove(); + notice.get().trigger("pnotify.cancel", [notice, value]); + } + }; self.timeoutPopupOptions = { title: gettext('System Shutdown'), + title_escape: true, + text_escape: true, type: 'notice', icon: true, hide: false, confirm: { confirm: true, - buttons: [{ - text: 'Abort Shutdown', - addClass: 'btn-block btn-danger', - promptTrigger: true, - click: function(notice, value){ - notice.remove(); - notice.get().trigger("pnotify.cancel", [notice, value]); - } - }] + buttons: [] }, buttons: { closer: false, @@ -62,6 +66,26 @@ $(function() { self.automaticShutdownEnabled.subscribe(self.onAutomaticShutdownEvent, self); + self.showTimeoutPopup = function(text) { + self.timeoutPopupOptions.text = text; + self.timeoutPopupOptions.confirm.buttons = + self.loginState.hasPermission(self.access.permissions.PLUGIN_AUTOMATICSHUTDOWN_ABORT) + ? [self.abortShutdownButton] : []; + if (typeof self.timeoutPopup != "undefined") { + self.timeoutPopup.update(self.timeoutPopupOptions); + } else { + self.timeoutPopup = new PNotify(self.timeoutPopupOptions); + self.timeoutPopup.get().on('pnotify.cancel', function() {self.abortShutdown(true);}); + } + }; + + self.hideTimeoutPopup = function() { + if (typeof self.timeoutPopup != "undefined") { + self.timeoutPopup.remove(); + self.timeoutPopup = undefined; + } + }; + self.onDataUpdaterPluginMessage = function(plugin, data) { if (plugin != "automaticshutdown") { return; @@ -71,28 +95,12 @@ $(function() { if (data.type == "timeout") { if ((data.timeout_value != null) && (data.timeout_value > 0)) { - self.timeoutPopupOptions.text = self.timeoutPopupShutdownText + data.timeout_value; - if (typeof self.timeoutPopup != "undefined") { - self.timeoutPopup.update(self.timeoutPopupOptions); - } else { - self.timeoutPopup = new PNotify(self.timeoutPopupOptions); - self.timeoutPopup.get().on('pnotify.cancel', function() {self.abortShutdown(true);}); - } + self.showTimeoutPopup(self.timeoutPopupShutdownText + data.timeout_value); } else { - if (typeof self.timeoutPopup != "undefined") { - self.timeoutPopup.remove(); - self.timeoutPopup = undefined; - } + self.hideTimeoutPopup(); } } else if (data.type == "timelapse_processing") { - self.timeoutPopupOptions.text = self.timeoutPopupTimelapseProcessingText; - if (typeof self.timeoutPopup != "undefined") { - self.timeoutPopup.update(self.timeoutPopupOptions); - } else { - self.timeoutPopup = new PNotify(self.timeoutPopupOptions); - self.timeoutPopup.get().on('pnotify.cancel', function() {self.abortShutdown(true);}); - } - + self.showTimeoutPopup(self.timeoutPopupTimelapseProcessingText); } } @@ -113,7 +121,7 @@ $(function() { OCTOPRINT_VIEWMODELS.push([ AutomaticShutdownViewModel, - ["loginStateViewModel"], + ["loginStateViewModel", "accessViewModel"], document.getElementById("sidebar_plugin_automaticshutdown") ]); }); diff --git a/octoprint_automaticshutdown/templates/automaticshutdown_sidebar.jinja2 b/octoprint_automaticshutdown/templates/automaticshutdown_sidebar.jinja2 index 8da1ef0..0645540 100644 --- a/octoprint_automaticshutdown/templates/automaticshutdown_sidebar.jinja2 +++ b/octoprint_automaticshutdown/templates/automaticshutdown_sidebar.jinja2 @@ -1,6 +1,7 @@ From f7467ec2829b9a1ff69ea8c0b1fbb0af7c056a46 Mon Sep 17 00:00:00 2001 From: Jacopo Tediosi Date: Tue, 5 May 2026 01:37:44 +0200 Subject: [PATCH 3/6] Remove unused imports --- octoprint_automaticshutdown/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/octoprint_automaticshutdown/__init__.py b/octoprint_automaticshutdown/__init__.py index 1f1063d..aa86854 100644 --- a/octoprint_automaticshutdown/__init__.py +++ b/octoprint_automaticshutdown/__init__.py @@ -1,8 +1,6 @@ # coding=utf-8 from __future__ import absolute_import -import time - import octoprint.plugin import octoprint.timelapse from flask import make_response From a5cf1f3b0f5e184633206ac69c04a8c4e6a1fd4f Mon Sep 17 00:00:00 2001 From: Jacopo Tediosi Date: Tue, 5 May 2026 01:39:35 +0200 Subject: [PATCH 4/6] Implement is_template_autoescaped Ref: https://docs.octoprint.org/en/dev/plugins/mixins.html#octoprint.plugin.TemplatePlugin.is_template_autoescaped --- octoprint_automaticshutdown/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/octoprint_automaticshutdown/__init__.py b/octoprint_automaticshutdown/__init__.py index aa86854..fee466f 100644 --- a/octoprint_automaticshutdown/__init__.py +++ b/octoprint_automaticshutdown/__init__.py @@ -53,6 +53,9 @@ def get_template_configs(self): dict(type="settings", custom_bindings=False), ] + def is_template_autoescaped(self): + return True + def get_api_commands(self): return dict(enable=[], disable=[], abort=[]) From 14a389f29e535569eab398825b8b9612a24682ef Mon Sep 17 00:00:00 2001 From: Jacopo Tediosi Date: Tue, 5 May 2026 01:39:52 +0200 Subject: [PATCH 5/6] Implement is_api_protected Ref: https://docs.octoprint.org/en/dev/plugins/mixins.html#octoprint.plugin.SimpleApiPlugin.is_api_protected --- octoprint_automaticshutdown/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/octoprint_automaticshutdown/__init__.py b/octoprint_automaticshutdown/__init__.py index fee466f..9b5ec85 100644 --- a/octoprint_automaticshutdown/__init__.py +++ b/octoprint_automaticshutdown/__init__.py @@ -59,6 +59,9 @@ def is_template_autoescaped(self): def get_api_commands(self): return dict(enable=[], disable=[], abort=[]) + def is_api_protected(self): + return True + def on_api_command(self, command, data): required_permissions = { "enable": Permissions.PLUGIN_AUTOMATICSHUTDOWN_ENABLE, From d74bba9cf61e6a4524c90d41e1018a7f168b7cef Mon Sep 17 00:00:00 2001 From: Jacopo Tediosi Date: Tue, 5 May 2026 01:42:31 +0200 Subject: [PATCH 6/6] Implement pyproject This was done with command "octoprint dev plugin:migrate-to-pyproject" and some additional manual adjustments on the generated files --- MANIFEST.in | 3 ++ Taskfile.yml | 87 ++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 42 ++++++++++++++++++++++ requirements.txt | 9 ----- setup.py | 94 ++---------------------------------------------- 5 files changed, 135 insertions(+), 100 deletions(-) create mode 100644 Taskfile.yml create mode 100644 pyproject.toml delete mode 100644 requirements.txt diff --git a/MANIFEST.in b/MANIFEST.in index bb3ec5f..8bd3b16 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1,4 @@ include README.md +recursive-include octoprint_automaticshutdown/templates * +recursive-include octoprint_automaticshutdown/translations * +recursive-include octoprint_automaticshutdown/static * \ No newline at end of file diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 0000000..11c8728 --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,87 @@ + +# Taskfile to be used with task: https://taskfile.dev +# +# A copy of task gets automatically installed as a "develop" dependency this plugin: +# +# pip install .[develop] +# go-task --list-all +# + +version: "3" + +env: + LOCALES: ['es'] # list your included locales here, e.g. ["de", "fr"] + TRANSLATIONS: "octoprint_automaticshutdown/translations" # translations folder + + +tasks: + install: + desc: Installs the plugin into the current venv + cmds: + - "python -m pip install -e .[develop]" + + ### Build related + + build: + desc: Builds sdist & wheel + cmds: + - python -m build --sdist --wheel + + build-sdist: + desc: Builds sdist + cmds: + - python -m build --sdist + + build-wheel: + desc: Builds wheel + cmds: + - python -m build --wheel + + ### Translation related + + babel-new: + desc: Create a new translation for a locale + cmds: + - task: babel-extract + - pybabel init --input-file=translations/messages.pot --output-dir=translations --locale="{{ .CLI_ARGS }}" + + babel-extract: + desc: Update pot file from source + cmds: + - pybabel extract --mapping-file=babel.cfg --output-file=translations/messages.pot --msgid-bugs-address=i18n@octoprint.org --copyright-holder="The OctoPrint Project" . + + babel-update: + desc: Update translation files from pot file + cmds: + - for: + var: LOCALES + cmd: pybabel update --input-file=translations/messages.pot --output-dir=translations --locale={{ .ITEM }} + + babel-refresh: + desc: Update translation files from source + cmds: + - task: babel-extract + - task: babel-update + + babel-compile: + desc: Compile translation files + cmds: + - pybabel compile --directory=translations + + babel-bundle: + desc: Bundle translations + preconditions: + - test -d {{ .TRANSLATIONS }} + cmds: + - for: + var: LOCALES + cmd: | + locale="{{ .ITEM }}" + source="translations/${locale}" + target="{{ .TRANSLATIONS }}/${locale}" + + [ ! -d "${target}" ] || rm -r "${target}" + + echo "Copying translations for locale ${locale} from ${source} to ${target}..." + cp -r "${source}" "${target}" + diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b5d7bff --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,42 @@ +[build-system] +requires = [ + "setuptools>=68", +] +build-backend = "setuptools.build_meta" + +[project] +name = "OctoPrint-AutomaticShutdown" +version = "0.1.5" +description = "Enables the system to be automatically shut down after a print is finished" +authors = [ + { name = "Nicanor Romero Venier", email = "nicanor.romerovenier@bq.com" }, +] +requires-python = ">=2.7,<4" +dependencies = [] +dynamic = [ + "license", +] + +[project.entry-points."octoprint.plugin"] +automaticshutdown = "octoprint_automaticshutdown" + +[project.urls] +Homepage = "https://github.com/OctoPrint/OctoPrint-AutomaticShutdown" + +[project.optional-dependencies] +develop = [ + "go-task-bin", +] + +[project.readme] +file = "README.md" +content-type = "text/markdown" + +[tool.setuptools] +include-package-data = true + +[tool.setuptools.packages.find] +include = [ + "octoprint_automaticshutdown", + "octoprint_automaticshutdown.*", +] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index a1dc463..0000000 --- a/requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -### -# This file is only here to make sure that something like -# -# pip install -e . -# -# works as expected. Requirements can be found in setup.py. -### - -. diff --git a/setup.py b/setup.py index 5a45b03..db2c138 100644 --- a/setup.py +++ b/setup.py @@ -1,94 +1,6 @@ -# coding=utf-8 -######################################################################################################################## -### Do not forget to adjust the following variables to your own plugin. +import setuptools -# The plugin's identifier, has to be unique -plugin_identifier = "automaticshutdown" +# we define the license string like this to be backwards compatible to setuptools<77 +setuptools.setup(license="AGPL-3.0-or-later") -# The plugin's python package, should be "octoprint_", has to be unique -plugin_package = "octoprint_automaticshutdown" - -# The plugin's human readable name. Can be overwritten within OctoPrint's internal data via __plugin_name__ in the -# plugin module -plugin_name = "OctoPrint-AutomaticShutdown" - -# The plugin's version. Can be overwritten within OctoPrint's internal data via __plugin_version__ in the plugin module -plugin_version = "0.1.5" - -# The plugin's description. Can be overwritten within OctoPrint's internal data via __plugin_description__ in the plugin -# module -plugin_description = "Enables the system to be automatically shut down after a print is finished" - -# The plugin's author. Can be overwritten within OctoPrint's internal data via __plugin_author__ in the plugin module -plugin_author = "Nicanor Romero Venier" - -# The plugin's author's mail address. -plugin_author_email = "nicanor.romerovenier@bq.com" - -# The plugin's homepage URL. Can be overwritten within OctoPrint's internal data via __plugin_url__ in the plugin module -plugin_url = "https://github.com/OctoPrint/OctoPrint-AutomaticShutdown" - -# The plugin's license. Can be overwritten within OctoPrint's internal data via __plugin_license__ in the plugin module -plugin_license = "AGPLv3" - -# Any additional requirements besides OctoPrint should be listed here -plugin_requires = [] - -### -------------------------------------------------------------------------------------------------------------------- -### More advanced options that you usually shouldn't have to touch follow after this point -### -------------------------------------------------------------------------------------------------------------------- - -# Additional package data to install for this plugin. The subfolders "templates", "static" and "translations" will -# already be installed automatically if they exist. -plugin_additional_data = [] - -# Any additional python packages you need to install with your plugin that are not contains in .* -plugin_addtional_packages = [] - -# Any python packages within .* you do NOT want to install with your plugin -plugin_ignored_packages = [] - -# Additional parameters for the call to setuptools.setup. If your plugin wants to register additional entry points, -# define dependency links or other things like that, this is the place to go. Will be merged recursively with the -# default setup parameters as provided by octoprint_setuptools.create_plugin_setup_parameters using -# octoprint.util.dict_merge. -# -# Example: -# plugin_requires = ["someDependency==dev"] -# additional_setup_parameters = {"dependency_links": ["https://github.com/someUser/someRepo/archive/master.zip#egg=someDependency-dev"]} -additional_setup_parameters = {} - -######################################################################################################################## - -from setuptools import setup - -try: - import octoprint_setuptools -except: - print("Could not import OctoPrint's setuptools, are you sure you are running that under " - "the same python installation that OctoPrint is installed under?") - import sys - sys.exit(-1) - -setup_parameters = octoprint_setuptools.create_plugin_setup_parameters( - identifier=plugin_identifier, - package=plugin_package, - name=plugin_name, - version=plugin_version, - description=plugin_description, - author=plugin_author, - mail=plugin_author_email, - url=plugin_url, - license=plugin_license, - requires=plugin_requires, - additional_packages=plugin_addtional_packages, - ignored_packages=plugin_ignored_packages, - additional_data=plugin_additional_data -) - -if len(additional_setup_parameters): - from octoprint.util import dict_merge - setup_parameters = dict_merge(setup_parameters, additional_setup_parameters) - -setup(**setup_parameters)