Ver Fonte

fix(webhook): stop and cancel through the API actually stop the print

maziggy há 1 semana atrás
pai
commit
c1afe21c90

+ 0 - 16
.basedpyright/baseline.json

@@ -3658,22 +3658,6 @@
                     "endColumn": 40,
                     "lineCount": 1
                 }
-            },
-            {
-                "code": "reportGeneralTypeIssues",
-                "range": {
-                    "startColumn": 14,
-                    "endColumn": 52,
-                    "lineCount": 1
-                }
-            },
-            {
-                "code": "reportAttributeAccessIssue",
-                "range": {
-                    "startColumn": 30,
-                    "endColumn": 42,
-                    "lineCount": 1
-                }
             }
         ],
         "./backend/app/core/auth.py": [

+ 3 - 0
CHANGELOG.md

@@ -161,6 +161,9 @@ All notable changes to Bambuddy will be documented in this file.
 - **The frontend build no longer warns about `path` and `crypto` being externalized for the STEP previewer (#2976)** — `occt-import-js`, the Emscripten build behind STEP previews, requires both modules, but only inside its `ENVIRONMENT_IS_NODE` branches; in the browser it loads its `.wasm` from the URL the preview worker passes and draws randomness from `crypto.getRandomValues`. Vite still externalized both and printed two warnings on every build. `vite.config.ts` now drops exactly those two warnings for that one package through `build.rolldownOptions.onLog`, so an externalization anywhere else, or of any other module, still shows.
 
 ### Fixed
+- **Stopping or cancelling a print through the API failed with an error** — `POST /api/v1/webhook/printer/{id}/cancel` answered with an error and left the print running. `POST /api/v1/webhook/printer/{id}/stop` stopped the print but still answered with an error. This affected Home Assistant automations and scripts that use an API key; the Stop button in Bambuddy was not affected.
+  - **Now both stop the print and answer normally.** If the printer drops off before the command can be sent, they answer "Printer not connected" (503).
+  - **A print stopped this way now counts as stopped, not failed,** as with the Stop button: the "print stopped" notification goes out instead of "print failed", and the print is no longer mislabelled with a printer error such as a layer shift.
 - **Starting the next plate of a Send All deleted the previous plate's timelapse (#3275, reported by @dovmesiz)** — All plates sent together with **Send All** share one archive. When a print starts on an archive that already has a timelapse, Bambuddy clears it so the new run gets its own video and completion photo. It also deleted the old video file, so starting plate 2 deleted plate 1's. With **delete from printer after archiving** on, that was the only copy. The video is now deleted only when the same plate prints again. Another plate's video, or one attached by hand or before this release, stays in the archive folder and is removed with the archive. The card still shows the latest plate's video. An external-camera timelapse no longer overwrites a kept video of the same name.
   - **A queued print still running 2 hours after it was sent lost what Bambuddy stored for it at dispatch:** its plate, AMS mapping and cost centre, which the completion uses (for example for the per-plate totals in the notification). A clean-up of unused print registrations removed them mid-print. They are now kept until the print completes.
 - **A PP spool without a slicer preset was sent to the AMS slot with no filament ID (#3273, reported by @sgiffhorn)** — When a spool has no preset of its own, Bambuddy fills in Bambu's generic preset for its material. The table used for spool assignment had no entry for PP, PE, PCTG, PLA Silk or PLA High Speed, so those slots got an empty filament ID, while **Configure AMS Slot** used a separate, longer table. The backend now has one table with all five, matching the one the frontend already used. A slot already set to Generic PLA Silk or PLA High Speed still keeps it when a PLA spool without a preset is assigned, as before.

+ 22 - 10
backend/app/api/routes/webhook.py

@@ -235,6 +235,26 @@ async def webhook_start_print(
     return {"message": "Print started", "queue_item_id": queue_item.id}
 
 
+def _stop_current_print(printer_id: int) -> None:
+    """Send the stop command and mark the printer as stopped by the user.
+
+    Cancel and stop are the same MQTT command on Bambu printers. The mark makes
+    the print end as stopped rather than failed, as a stop from the printer
+    card or the queue does (mark_printer_stopped_by_user).
+    """
+    try:
+        sent = printer_manager.stop_print(printer_id)
+    except Exception as e:
+        logger.error("Failed to stop print on printer %s: %s", printer_id, e)
+        raise HTTPException(status_code=500, detail=str(e))
+    if not sent:
+        raise HTTPException(status_code=503, detail="Printer not connected")
+
+    from backend.app.main import mark_printer_stopped_by_user
+
+    mark_printer_stopped_by_user(printer_id)
+
+
 @router.post("/printer/{printer_id}/stop")
 async def webhook_stop_print(
     printer_id: int,
@@ -258,11 +278,7 @@ async def webhook_stop_print(
     if status.state != "RUNNING":
         raise HTTPException(status_code=409, detail="No print in progress")
 
-    try:
-        await printer_manager.stop_print(printer_id)
-    except Exception as e:
-        logger.error("Failed to stop print: %s", e)
-        raise HTTPException(status_code=500, detail=str(e))
+    _stop_current_print(printer_id)
 
     return {"message": "Print stopped"}
 
@@ -288,11 +304,7 @@ async def webhook_cancel_print(
     if status.state not in ["RUNNING", "PAUSE"]:
         raise HTTPException(status_code=409, detail="No print to cancel")
 
-    try:
-        await printer_manager.cancel_print(printer_id)
-    except Exception as e:
-        logger.error("Failed to cancel print: %s", e)
-        raise HTTPException(status_code=500, detail=str(e))
+    _stop_current_print(printer_id)
 
     return {"message": "Print cancelled"}
 

+ 60 - 0
backend/tests/integration/test_webhook_printer_status.py

@@ -398,3 +398,63 @@ class TestWebhookCancelPrint:
                 headers={"X-API-Key": api_key_data},
             )
         assert resp.status_code == 409
+
+
+async def _send(async_client: AsyncClient, key: str, printer_id: int, action: str, state: str, stop_result: bool):
+    """POST stop/cancel with a live-looking printer; returns (response, stop_print mock)."""
+    stop_print = MagicMock(return_value=stop_result)
+    with (
+        patch(
+            "backend.app.api.routes.webhook.printer_manager.get_status",
+            MagicMock(return_value=PrinterState(connected=True, state=state)),
+        ),
+        patch("backend.app.api.routes.webhook.printer_manager.stop_print", stop_print),
+    ):
+        resp = await async_client.post(
+            f"/api/v1/webhook/printer/{printer_id}/{action}",
+            headers={"X-API-Key": key},
+        )
+    return resp, stop_print
+
+
+class TestWebhookStopAndCancelSendTheStop:
+    """Both routes send the stop and mark the printer as stopped by the user.
+
+    Pre-fix, /stop awaited the synchronous ``stop_print`` (the stop went out,
+    then ``await True`` raised and the route answered 500) and /cancel called a
+    ``cancel_print`` that does not exist (500, print kept running). Neither
+    marked the printer, so the print ended up classified as failed instead of
+    stopped, like a stop from the printer card does.
+    """
+
+    @pytest.fixture(autouse=True)
+    def _clear_user_stopped(self):
+        from backend.app.main import _user_stopped_printers
+
+        _user_stopped_printers.clear()
+        yield
+        _user_stopped_printers.clear()
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    @pytest.mark.parametrize(("action", "state"), [("stop", "RUNNING"), ("cancel", "RUNNING"), ("cancel", "PAUSE")])
+    async def test_sends_the_stop_and_marks_the_printer(self, async_client, api_key_data, printer_row, action, state):
+        from backend.app.main import _user_stopped_printers
+
+        resp, stop_print = await _send(async_client, api_key_data, printer_row.id, action, state, stop_result=True)
+
+        assert resp.status_code == 200, resp.text
+        stop_print.assert_called_once_with(printer_row.id)
+        assert printer_row.id in _user_stopped_printers
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    @pytest.mark.parametrize("action", ["stop", "cancel"])
+    async def test_reports_503_when_the_stop_cannot_be_sent(self, async_client, api_key_data, printer_row, action):
+        from backend.app.main import _user_stopped_printers
+
+        resp, stop_print = await _send(async_client, api_key_data, printer_row.id, action, "RUNNING", stop_result=False)
+
+        assert resp.status_code == 503, resp.text
+        stop_print.assert_called_once_with(printer_row.id)
+        assert printer_row.id not in _user_stopped_printers