Files
bambuddy/backend/tests/integration/test_drying_screen_only.py
maziggy ca3f6e5ee0 fix(drying): P1 AMS drying is screen-only — stop offering it (#2533)
The reporter found what his P1S was doing, and it is in Bambu's P1 manual:
"P1S connected AMS drying functions may only be controlled from the P1S screen."
The firmware acks ams_filament_drying with result: success and then discards it,
which is why three commands on an idle printer left the AMS 2 Pro at dry_status 0.
No command can start a cycle on a P1, on any firmware, so don't offer one.

supports_drying() now excludes the P1 series outright, replacing the 01.08+ gate
carried since #292 — that version is when P1 firmware gained AMS 2 Pro support,
not remote drying, and it was never checked against a live P1. Both drying routes
refuse with a specific 400 instead of publishing a message the printer will drop;
queue and ambient auto-drying skip P1s via the same helper.

A new drying_screen_only flag keeps the control on the card, disabled, saying why
— a P1 owner needs to learn where to dry, not watch the button disappear. A cycle
started at the printer still shows with its countdown; only Stop goes away, since
a P1 ignores stop exactly as it ignores start.

Also corrects the wiki firmware matrix, which listed P1P/P1S as supported and
(separately) P2S/H2S/H2C as unsupported. 8 tests.
2026-07-11 09:33:52 +02:00

71 lines
2.5 KiB
Python

"""P1-series AMS drying is screen-only — the API must refuse it (#2533).
Bambu's P1 manual states that "P1S connected AMS drying functions may only be
controlled from the P1S screen". The firmware still answers
``ams_filament_drying`` with ``result: success`` and then ignores it, which is
exactly what the reporter saw: three commands accepted on an idle P1S with an
AMS 2 Pro, and the unit never left ``dry_status: 0``.
So a command we can't fulfil must be refused rather than acked, and that has to
hold for stop as well as start — a cycle a P1S user started at the printer can
only be ended there.
"""
from unittest.mock import MagicMock, patch
import pytest
from httpx import AsyncClient
@pytest.fixture
def mqtt_send():
"""Watch the MQTT command so we can assert nothing was published."""
with patch(
"backend.app.services.printer_manager.printer_manager.send_drying_command",
new=MagicMock(return_value=True),
) as m:
yield m
@pytest.fixture
def live_state():
"""A connected printer on firmware new enough that only the model gates drying."""
state = MagicMock()
state.firmware_version = "01.10.00.00"
state.raw_data = {"ams": [{"id": 0, "module_type": "n3f", "tray": []}]}
with patch(
"backend.app.services.printer_manager.printer_manager.get_status",
new=MagicMock(return_value=state),
) as m:
yield m
@pytest.mark.asyncio
@pytest.mark.integration
@pytest.mark.parametrize("model", ["P1S", "P1P"])
@pytest.mark.parametrize("action", ["start", "stop"])
async def test_screen_only_model_refuses_drying(
async_client: AsyncClient, printer_factory, mqtt_send, live_state, model, action
):
printer = await printer_factory(model=model)
response = await async_client.post(f"/api/v1/printers/{printer.id}/drying/{action}?ams_id=0")
assert response.status_code == 400
assert "screen" in response.json()["detail"].lower()
# And nothing went out on the wire — an ack the printer would drop is worse
# than a refusal, because it leaves the user believing drying is running.
mqtt_send.assert_not_called()
@pytest.mark.asyncio
@pytest.mark.integration
@pytest.mark.parametrize("action", ["start", "stop"])
async def test_commandable_model_still_dries(async_client: AsyncClient, printer_factory, mqtt_send, live_state, action):
printer = await printer_factory(model="X1C")
response = await async_client.post(f"/api/v1/printers/{printer.id}/drying/{action}?ams_id=0")
assert response.status_code == 200
mqtt_send.assert_called_once()