From 591cf002a6a086367ae999c5fbea2a0b0dd4eb5a Mon Sep 17 00:00:00 2001 From: Ajimaru Date: Fri, 11 Sep 2026 13:29:55 +0200 Subject: [PATCH 1/2] feat: add conflict detection for PortRetry plugins and update documentation --- README.md | 34 +++++++------- extras/autoconnectplus.md | 6 +++ octoprint_autoconnectplus/__init__.py | 44 ++++++++++++++++--- .../static/js/autoconnectplus.js | 34 +++++++++++++- pyproject.toml | 2 +- tests/test_plugin.py | 40 +++++++++++++++++ 6 files changed, 136 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 2a78c26..f99b090 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,13 @@ Automatically (re)connect your printer in OctoPrint — not only over **serial** also through the OctoPrint 2.0 **connector framework** for **Moonraker (Klipper)**, **Bambu** and any other registered connector. +> [!NOTE] +> **About this project.** I built this for my own printer setup with AI, and if +> it helps others, even better. I have tested it to the best of my knowledge and +> ability, and every change is backed by an automated test suite, CI, and +> security scans (Bandit, CodeQL). Disclosed here per the OctoPrint plugin guidelines. +> Issues and PRs are welcome. + AutoConnectPlus is a fork of [OctoPrint-PortRetryPlus](https://github.com/hprombex/OctoPrint-PortRetryPlus) that keeps its proven retry/timer logic and extends it to the modern connector API. @@ -44,6 +51,12 @@ OctoPrint stores a single preferred connection, so there is never any ambiguity. it is missing or incomplete (or the matching connector plugin is not installed), the plugin simply keeps waiting and logs the reason once instead of every interval. +**Only one reconnect plugin should be active.** AutoConnectPlus detects the original +`PortRetry` plugin (`portretry`, by vehystrix) and the `PortRetryPlus` fork +(`portretryplus`, by hprombex). If either one is enabled at the same time, AutoConnectPlus +shows a permanent error toast as soon as the OctoPrint interface loads, because both +plugins can compete to reconnect the same printer. + ## Requirements - **Serial** mode works on any reasonably recent OctoPrint (1.x included), where it @@ -83,21 +96,14 @@ target, refreshed every time the dialog opens) and these options: The printer profile used is OctoPrint's default profile. -The same options can be set in `~/.octoprint/config.yaml`: - -```yaml -plugins: - autoconnectplus: - enabled: true # master switch - interval: 5.0 # seconds between retries (minimum 0.1) - forced_port: "" # serial only: used when OctoPrint's port is unset/AUTO -``` - ## Troubleshooting All plugin activity is logged to `octoprint.log`, prefixed with `octoprint.plugins.autoconnectplus`. +- **A permanent error toast mentions PortRetry** — disable either AutoConnectPlus, + PortRetry, or PortRetryPlus. Only one automatic reconnect plugin should be active; + installing both is supported, running both is not. - **Nothing reconnects at all** — check that the plugin is enabled in its settings and that the *detected connection* shown there is the one you expect. If a warning is shown instead (no port detected, no preferred connection stored, connector @@ -145,15 +151,9 @@ rolling `latest` release behind the stable install URL above. - Original [OctoPrint-PortRetryPlus](https://github.com/hprombex/OctoPrint-PortRetryPlus) by **hprombex**. -- Earlier work and inspiration credited to **vehystrix**. +- Earlier work and inspiration from [OctoPrint-PortRetry](https://github.com/vehystrix/OctoPrint-PortRetry) credited to **vehystrix**. ## License Licensed under the **GNU Affero General Public License v3 or later (AGPL-3.0-or-later)**, matching the original project. See [LICENSE](LICENSE). - -> [!NOTE] -> **About this project.** I built this for my own printer setup with AI, and if -> it helps others, even better. I have tested it to the best of my knowledge and -> ability. Disclosed here per the OctoPrint plugin guidelines. -> Issues and PRs are welcome. diff --git a/extras/autoconnectplus.md b/extras/autoconnectplus.md index cb5eda6..848626f 100644 --- a/extras/autoconnectplus.md +++ b/extras/autoconnectplus.md @@ -47,6 +47,12 @@ Offline printers are detected with a quick reachability probe and skipped quietly, and repeated failed attempts back off progressively instead of flooding the log. +Only one automatic reconnect plugin should be active. AutoConnectPlus checks for +both `portretry` (the original OctoPrint-PortRetry by vehystrix) and `portretryplus` +(the OctoPrint-PortRetryPlus fork by hprombex). If either is enabled, it shows a +permanent error toast immediately when the OctoPrint interface loads, because two +reconnect plugins can compete for the same printer connection. + AutoConnectPlus is a fork of [OctoPrint-PortRetryPlus](https://github.com/hprombex/OctoPrint-PortRetryPlus) by hprombex (with earlier work credited to vehystrix). The serial retry/timer logic is carried over; the connector support is new. diff --git a/octoprint_autoconnectplus/__init__.py b/octoprint_autoconnectplus/__init__.py index bdf61d3..b3b4049 100644 --- a/octoprint_autoconnectplus/__init__.py +++ b/octoprint_autoconnectplus/__init__.py @@ -426,10 +426,32 @@ def _host_reachable(self, connector: str, parameters: dict) -> bool: # Detected connection (settings display / simple API) # ------------------------------------------------------------------ # - def _detected_connection(self) -> dict[str, str]: + def _portretry_plugins_enabled(self) -> list[str]: + """Return the enabled PortRetry plugins that can conflict.""" + plugin_names = { + "portretry": "PortRetry", + "portretryplus": "PortRetryPlus", + } + try: + plugin_manager = octoprint.plugin.plugin_manager() + return [ + name + for identifier, name in plugin_names.items() + if plugin_manager.get_plugin_info( + identifier, require_enabled=True + ) + is not None + ] + except Exception: # pylint: disable=broad-exception-caught + # Plugin discovery must never prevent the settings API from + # returning the detected connection. + return [] + + def _detected_connection(self) -> dict[str, Any]: """Describe the connection to reconnect, for the settings display: label, target (serial port or host:port) and an optional warning.""" connector = self._get_preferred_connector() + portretry_plugins = self._portretry_plugins_enabled() label = CONNECTOR_LABELS.get(connector, connector) if self._is_serial_connector(connector): @@ -438,7 +460,12 @@ def _detected_connection(self) -> dict[str, str]: "No serial port detected yet; set one in OctoPrint's " "connection dialog or configure a forced port below." ) - return {"label": label, "target": target, "warning": warning} + return { + "label": label, + "target": target, + "warning": warning, + "portretry_plugins": portretry_plugins, + } parameters = self._get_preferred_parameters() host = parameters.get("host", "") @@ -459,9 +486,14 @@ def _detected_connection(self) -> dict[str, str]: "matching connector plugin." ) - return {"label": label, "target": target, "warning": warning} + return { + "label": label, + "target": target, + "warning": warning, + "portretry_plugins": portretry_plugins, + } - def on_api_get(self, request): + def on_api_get(self, request): # type: ignore[override] """Serve the detected connection to the settings dialog, which fetches it every time it is shown so the display never goes stale.""" return flask.jsonify(self._detected_connection()) @@ -552,7 +584,9 @@ def on_settings_save(self, data) -> dict[Any, Any]: # Match the entry-point key so the runtime identifier is explicit (otherwise # defaults to the package name). __plugin_identifier__ = "autoconnectplus" -__plugin_author__ = "ajimaru" +__plugin_author__ = ( + "ajimaru, based on work from vehystrix and hprombex" +) __plugin_description__ = ( "Automatically reconnects the printer over serial, Moonraker or Bambu " "connectors" diff --git a/octoprint_autoconnectplus/static/js/autoconnectplus.js b/octoprint_autoconnectplus/static/js/autoconnectplus.js index aba81fc..45cad29 100644 --- a/octoprint_autoconnectplus/static/js/autoconnectplus.js +++ b/octoprint_autoconnectplus/static/js/autoconnectplus.js @@ -9,6 +9,36 @@ * stale after the preferred connection changes. */ $(function () { + var portRetryWarningShown = false; + + function showPortRetryWarning(plugins) { + if (portRetryWarningShown || typeof PNotify === "undefined") { + return; + } + + new PNotify({ + title: "AutoConnectPlus error", + text: plugins.join(" and ") + " is also enabled. Disable " + + "one of the reconnect plugins to prevent competing " + + "reconnect attempts.", + type: "error", + hide: false, + }); + portRetryWarningShown = true; + } + + function checkPortRetryConflict() { + OctoPrint.simpleApiGet("autoconnectplus").done(function (data) { + if (data.portretry_plugins && data.portretry_plugins.length) { + showPortRetryWarning(data.portretry_plugins); + } + }); + } + + // Check immediately after the global plugin assets are ready, not only + // when the user opens the settings dialog. + checkPortRetryConflict(); + function AutoConnectPlusViewModel(parameters) { var self = this; @@ -17,12 +47,14 @@ $(function () { self.detectedLabel = ko.observable(""); self.detectedTarget = ko.observable(""); self.detectedWarning = ko.observable(""); - self.refreshDetected = function () { OctoPrint.simpleApiGet("autoconnectplus").done(function (data) { self.detectedLabel(data.label || ""); self.detectedTarget(data.target || ""); self.detectedWarning(data.warning || ""); + if (data.portretry_plugins && data.portretry_plugins.length) { + showPortRetryWarning(data.portretry_plugins); + } }); }; diff --git a/pyproject.toml b/pyproject.toml index 7309fff..59dd2b0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "OctoPrint-AutoConnectPlus" -version = "0.1.0rc3" +version = "0.1.0" description = "Automatically reconnects the printer over serial, Moonraker or Bambu connectors" authors = [ {name = "ajimaru", email = "ajimaru_gdr@pm.me"} diff --git a/tests/test_plugin.py b/tests/test_plugin.py index c1e8beb..faf3194 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -388,6 +388,7 @@ def test_detected_connection_serial_with_port(plugin): "label": "Serial", "target": "/dev/ttyUSB0", "warning": "", + "portretry_plugins": [], } @@ -396,6 +397,45 @@ def test_detected_connection_serial_without_port_warns(plugin): assert detected["label"] == "Serial" assert detected["target"] == "" assert "No serial port" in detected["warning"] + assert detected["portretry_plugins"] == [] + + +@pytest.mark.parametrize( + ("identifier", "display_name"), + [("portretry", "PortRetry"), ("portretryplus", "PortRetryPlus")], +) +def test_detected_connection_warns_when_portretry_plugin_is_enabled( + plugin, identifier, display_name +): + plugin_manager = mock.Mock() + + def get_plugin_info(checked_identifier, **_): + return mock.Mock() if checked_identifier == identifier else None + + plugin_manager.get_plugin_info.side_effect = get_plugin_info + with mock.patch( + "octoprint_autoconnectplus.octoprint.plugin.plugin_manager", + return_value=plugin_manager, + ): + detected = plugin._detected_connection() + + assert detected["portretry_plugins"] == [display_name] + assert plugin_manager.get_plugin_info.call_count == 2 + + +def test_detected_connection_warns_when_both_portretry_plugins_are_enabled( + plugin, +): + plugin_manager = mock.Mock() + plugin_manager.get_plugin_info.return_value = mock.Mock() + with mock.patch( + "octoprint_autoconnectplus.octoprint.plugin.plugin_manager", + return_value=plugin_manager, + ): + detected = plugin._detected_connection() + + assert detected["portretry_plugins"] == ["PortRetry", "PortRetryPlus"] + assert plugin_manager.get_plugin_info.call_count == 2 def test_detected_connection_connector_with_default_port( From 7d60bafd1e7b92710850f13f48d62e1b4f8c81d0 Mon Sep 17 00:00:00 2001 From: Ajimaru Date: Fri, 11 Sep 2026 13:32:04 +0200 Subject: [PATCH 2/2] chore: update dependabot configuration for GitHub Actions and pip dependencies --- .github/dependabot.yml | 45 +++++++++++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d698d8d..d79a84f 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,12 +1,39 @@ -# To get started with Dependabot version updates, you'll need to specify which -# package ecosystems to update and where the package manifests are located. -# Please see the documentation for all configuration options: -# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file - version: 2 updates: - - package-ecosystem: "" # See documentation for possible values - directory: "/" # Location of package manifests + - package-ecosystem: github-actions + directory: "/" + target-branch: dev + schedule: + interval: monthly + open-pull-requests-limit: 5 + groups: + github-actions: + patterns: + - "*" + + - package-ecosystem: pip + directory: "/" + target-branch: dev schedule: - interval: "weekly" - + interval: monthly + open-pull-requests-limit: 5 + groups: + dev-dependencies: + dependency-type: development + patterns: + - "*" + # Major bumps are deliberately not listed here: they stay individual + # PRs, since those are the ones that actually break things. + production-dependencies: + dependency-type: production + patterns: + - "*" + update-types: + - minor + # Patch bumps are ignored to keep PR volume low; security fixes still + # arrive through Dependabot security alerts, which ignore this filter. + ignore: + - dependency-name: "*" + update-types: + - version-update:semver-patch +