|
@@ -436,10 +436,38 @@ class TestAutoStopOnNoScheduledItems:
|
|
|
def scheduler(self):
|
|
def scheduler(self):
|
|
|
return PrintScheduler()
|
|
return PrintScheduler()
|
|
|
|
|
|
|
|
|
|
+ @staticmethod
|
|
|
|
|
+ def _make_setting(value):
|
|
|
|
|
+ s = MagicMock()
|
|
|
|
|
+ s.value = value
|
|
|
|
|
+ return s
|
|
|
|
|
+
|
|
|
|
|
+ @staticmethod
|
|
|
|
|
+ def _make_db_side_effect(settings_map):
|
|
|
|
|
+ """Create a side_effect for db.execute that returns settings by key."""
|
|
|
|
|
+
|
|
|
|
|
+ async def side_effect(stmt):
|
|
|
|
|
+ result = MagicMock()
|
|
|
|
|
+ try:
|
|
|
|
|
+ compiled = stmt.compile(compile_kwargs={"literal_binds": False})
|
|
|
|
|
+ param_values = list(compiled.params.values())
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ param_values = []
|
|
|
|
|
+
|
|
|
|
|
+ for key, val in settings_map.items():
|
|
|
|
|
+ if key in param_values:
|
|
|
|
|
+ result.scalar_one_or_none.return_value = val
|
|
|
|
|
+ return result
|
|
|
|
|
+
|
|
|
|
|
+ result.scalar_one_or_none.return_value = None
|
|
|
|
|
+ return result
|
|
|
|
|
+
|
|
|
|
|
+ return side_effect
|
|
|
|
|
+
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.asyncio
|
|
|
@patch("backend.app.services.print_scheduler.printer_manager")
|
|
@patch("backend.app.services.print_scheduler.printer_manager")
|
|
|
async def test_stops_when_no_scheduled_items(self, mock_pm, scheduler):
|
|
async def test_stops_when_no_scheduled_items(self, mock_pm, scheduler):
|
|
|
- """Auto-drying stops when queue has no scheduled items."""
|
|
|
|
|
|
|
+ """Auto-drying stops when queue has no scheduled items (queue mode only)."""
|
|
|
scheduler._drying_in_progress = {1: time.monotonic()}
|
|
scheduler._drying_in_progress = {1: time.monotonic()}
|
|
|
|
|
|
|
|
state = MagicMock()
|
|
state = MagicMock()
|
|
@@ -447,19 +475,11 @@ class TestAutoStopOnNoScheduledItems:
|
|
|
mock_pm.get_status.return_value = state
|
|
mock_pm.get_status.return_value = state
|
|
|
|
|
|
|
|
db = AsyncMock()
|
|
db = AsyncMock()
|
|
|
- # queue_drying_enabled = true
|
|
|
|
|
- enabled_setting = MagicMock()
|
|
|
|
|
- enabled_setting.value = "true"
|
|
|
|
|
-
|
|
|
|
|
- call_count = [0]
|
|
|
|
|
-
|
|
|
|
|
- async def db_execute(stmt):
|
|
|
|
|
- call_count[0] += 1
|
|
|
|
|
- result = MagicMock()
|
|
|
|
|
- result.scalar_one_or_none.return_value = enabled_setting
|
|
|
|
|
- return result
|
|
|
|
|
-
|
|
|
|
|
- db.execute = AsyncMock(side_effect=db_execute)
|
|
|
|
|
|
|
+ settings_returns = {
|
|
|
|
|
+ "queue_drying_enabled": self._make_setting("true"),
|
|
|
|
|
+ "ambient_drying_enabled": self._make_setting("false"),
|
|
|
|
|
+ }
|
|
|
|
|
+ db.execute = AsyncMock(side_effect=self._make_db_side_effect(settings_returns))
|
|
|
|
|
|
|
|
# Manual-start items only (no scheduled_time)
|
|
# Manual-start items only (no scheduled_time)
|
|
|
item = MagicMock()
|
|
item = MagicMock()
|
|
@@ -476,7 +496,7 @@ class TestAutoStopOnNoScheduledItems:
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.asyncio
|
|
|
@patch("backend.app.services.print_scheduler.printer_manager")
|
|
@patch("backend.app.services.print_scheduler.printer_manager")
|
|
|
async def test_stops_when_empty_queue(self, mock_pm, scheduler):
|
|
async def test_stops_when_empty_queue(self, mock_pm, scheduler):
|
|
|
- """Auto-drying stops when queue is completely empty."""
|
|
|
|
|
|
|
+ """Auto-drying stops when queue is completely empty (queue mode only)."""
|
|
|
scheduler._drying_in_progress = {1: time.monotonic()}
|
|
scheduler._drying_in_progress = {1: time.monotonic()}
|
|
|
|
|
|
|
|
state = MagicMock()
|
|
state = MagicMock()
|
|
@@ -484,11 +504,11 @@ class TestAutoStopOnNoScheduledItems:
|
|
|
mock_pm.get_status.return_value = state
|
|
mock_pm.get_status.return_value = state
|
|
|
|
|
|
|
|
db = AsyncMock()
|
|
db = AsyncMock()
|
|
|
- enabled_setting = MagicMock()
|
|
|
|
|
- enabled_setting.value = "true"
|
|
|
|
|
- result_mock = MagicMock()
|
|
|
|
|
- result_mock.scalar_one_or_none.return_value = enabled_setting
|
|
|
|
|
- db.execute = AsyncMock(return_value=result_mock)
|
|
|
|
|
|
|
+ settings_returns = {
|
|
|
|
|
+ "queue_drying_enabled": self._make_setting("true"),
|
|
|
|
|
+ "ambient_drying_enabled": self._make_setting("false"),
|
|
|
|
|
+ }
|
|
|
|
|
+ db.execute = AsyncMock(side_effect=self._make_db_side_effect(settings_returns))
|
|
|
|
|
|
|
|
await scheduler._check_auto_drying(db, [], set())
|
|
await scheduler._check_auto_drying(db, [], set())
|
|
|
|
|
|
|
@@ -518,3 +538,349 @@ class TestDryingTrackingTimestamps:
|
|
|
scheduler._drying_in_progress[1] = time.monotonic()
|
|
scheduler._drying_in_progress[1] = time.monotonic()
|
|
|
assert scheduler._drying_in_progress.get(1)
|
|
assert scheduler._drying_in_progress.get(1)
|
|
|
assert not scheduler._drying_in_progress.get(999)
|
|
assert not scheduler._drying_in_progress.get(999)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+class _DryingTestBase:
|
|
|
|
|
+ """Shared helpers for auto-drying integration tests."""
|
|
|
|
|
+
|
|
|
|
|
+ @staticmethod
|
|
|
|
|
+ def _make_setting(value):
|
|
|
|
|
+ s = MagicMock()
|
|
|
|
|
+ s.value = value
|
|
|
|
|
+ return s
|
|
|
|
|
+
|
|
|
|
|
+ @staticmethod
|
|
|
|
|
+ def _make_db_side_effect(settings_map, printer_ids=None):
|
|
|
|
|
+ """Create a side_effect for db.execute that returns settings by key and printers."""
|
|
|
|
|
+ if printer_ids is None:
|
|
|
|
|
+ printer_ids = [1]
|
|
|
|
|
+
|
|
|
|
|
+ async def side_effect(stmt):
|
|
|
|
|
+ result = MagicMock()
|
|
|
|
|
+ stmt_str = str(stmt)
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ compiled = stmt.compile(compile_kwargs={"literal_binds": False})
|
|
|
|
|
+ param_values = list(compiled.params.values())
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ param_values = []
|
|
|
|
|
+
|
|
|
|
|
+ for key, val in settings_map.items():
|
|
|
|
|
+ if key in param_values:
|
|
|
|
|
+ result.scalar_one_or_none.return_value = val
|
|
|
|
|
+ return result
|
|
|
|
|
+
|
|
|
|
|
+ if "printer" in stmt_str.lower() or "is_active" in stmt_str:
|
|
|
|
|
+ printers = []
|
|
|
|
|
+ for pid in printer_ids:
|
|
|
|
|
+ p = MagicMock()
|
|
|
|
|
+ p.id = pid
|
|
|
|
|
+ p.is_active = True
|
|
|
|
|
+ printers.append(p)
|
|
|
|
|
+ scalars_mock = MagicMock()
|
|
|
|
|
+ scalars_mock.__iter__ = MagicMock(return_value=iter(printers))
|
|
|
|
|
+ result.scalars.return_value = scalars_mock
|
|
|
|
|
+ else:
|
|
|
|
|
+ result.scalar_one_or_none.return_value = None
|
|
|
|
|
+ return result
|
|
|
|
|
+
|
|
|
|
|
+ return side_effect
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+class TestAmbientDrying(_DryingTestBase):
|
|
|
|
|
+ """Tests for ambient drying mode — drying based on humidity regardless of queue state."""
|
|
|
|
|
+
|
|
|
|
|
+ @pytest.fixture
|
|
|
|
|
+ def scheduler(self):
|
|
|
|
|
+ return PrintScheduler()
|
|
|
|
|
+
|
|
|
|
|
+ @pytest.mark.asyncio
|
|
|
|
|
+ @patch("backend.app.services.print_scheduler.printer_manager")
|
|
|
|
|
+ @patch("backend.app.services.print_scheduler.supports_drying", return_value=True)
|
|
|
|
|
+ async def test_ambient_dries_idle_printer_without_queue(self, mock_sd, mock_pm, scheduler):
|
|
|
|
|
+ """Ambient mode starts drying on idle printers even with no queue items."""
|
|
|
|
|
+ state = MagicMock()
|
|
|
|
|
+ state.raw_data = {
|
|
|
|
|
+ "ams": [
|
|
|
|
|
+ {
|
|
|
|
|
+ "id": 0,
|
|
|
|
|
+ "module_type": "n3f",
|
|
|
|
|
+ "dry_time": 0,
|
|
|
|
|
+ "humidity_raw": "75",
|
|
|
|
|
+ "dry_sf_reason": [],
|
|
|
|
|
+ "tray": [{"tray_type": "PLA"}],
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+ }
|
|
|
|
|
+ state.firmware_version = "01.09.00.00"
|
|
|
|
|
+ mock_pm.get_status.return_value = state
|
|
|
|
|
+ mock_pm.is_connected.return_value = True
|
|
|
|
|
+ mock_pm.get_model.return_value = "X1C"
|
|
|
|
|
+ mock_pm.send_drying_command.return_value = True
|
|
|
|
|
+
|
|
|
|
|
+ scheduler._is_printer_idle = MagicMock(return_value=True)
|
|
|
|
|
+ db = AsyncMock()
|
|
|
|
|
+
|
|
|
|
|
+ settings_returns = {
|
|
|
|
|
+ "queue_drying_enabled": self._make_setting("false"),
|
|
|
|
|
+ "ambient_drying_enabled": self._make_setting("true"),
|
|
|
|
|
+ "ams_humidity_fair": self._make_setting("60"),
|
|
|
|
|
+ "queue_drying_block": self._make_setting("false"),
|
|
|
|
|
+ "drying_presets": None,
|
|
|
|
|
+ }
|
|
|
|
|
+ db.execute = AsyncMock(side_effect=self._make_db_side_effect(settings_returns))
|
|
|
|
|
+
|
|
|
|
|
+ # Empty queue — ambient mode should still dry
|
|
|
|
|
+ await scheduler._check_auto_drying(db, [], set())
|
|
|
|
|
+
|
|
|
|
|
+ mock_pm.send_drying_command.assert_called_once_with(1, 0, 45, 12, mode=1, filament="PLA")
|
|
|
|
|
+ assert 1 in scheduler._drying_in_progress
|
|
|
|
|
+
|
|
|
|
|
+ @pytest.mark.asyncio
|
|
|
|
|
+ @patch("backend.app.services.print_scheduler.printer_manager")
|
|
|
|
|
+ @patch("backend.app.services.print_scheduler.supports_drying", return_value=True)
|
|
|
|
|
+ async def test_ambient_does_not_dry_below_threshold(self, mock_sd, mock_pm, scheduler):
|
|
|
|
|
+ """Ambient mode does NOT dry when humidity is below threshold."""
|
|
|
|
|
+ state = MagicMock()
|
|
|
|
|
+ state.raw_data = {
|
|
|
|
|
+ "ams": [
|
|
|
|
|
+ {
|
|
|
|
|
+ "id": 0,
|
|
|
|
|
+ "module_type": "n3f",
|
|
|
|
|
+ "dry_time": 0,
|
|
|
|
|
+ "humidity_raw": "40",
|
|
|
|
|
+ "dry_sf_reason": [],
|
|
|
|
|
+ "tray": [{"tray_type": "PLA"}],
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+ }
|
|
|
|
|
+ state.firmware_version = "01.09.00.00"
|
|
|
|
|
+ mock_pm.get_status.return_value = state
|
|
|
|
|
+ mock_pm.is_connected.return_value = True
|
|
|
|
|
+ mock_pm.get_model.return_value = "X1C"
|
|
|
|
|
+
|
|
|
|
|
+ scheduler._is_printer_idle = MagicMock(return_value=True)
|
|
|
|
|
+ db = AsyncMock()
|
|
|
|
|
+
|
|
|
|
|
+ settings_returns = {
|
|
|
|
|
+ "queue_drying_enabled": self._make_setting("false"),
|
|
|
|
|
+ "ambient_drying_enabled": self._make_setting("true"),
|
|
|
|
|
+ "ams_humidity_fair": self._make_setting("60"),
|
|
|
|
|
+ "queue_drying_block": self._make_setting("false"),
|
|
|
|
|
+ "drying_presets": None,
|
|
|
|
|
+ }
|
|
|
|
|
+ db.execute = AsyncMock(side_effect=self._make_db_side_effect(settings_returns))
|
|
|
|
|
+
|
|
|
|
|
+ await scheduler._check_auto_drying(db, [], set())
|
|
|
|
|
+
|
|
|
|
|
+ mock_pm.send_drying_command.assert_not_called()
|
|
|
|
|
+
|
|
|
|
|
+ @pytest.mark.asyncio
|
|
|
|
|
+ @patch("backend.app.services.print_scheduler.printer_manager")
|
|
|
|
|
+ async def test_ambient_off_stops_drying_without_queue(self, mock_pm, scheduler):
|
|
|
|
|
+ """Disabling ambient drying stops drying on printers without queue items."""
|
|
|
|
|
+ scheduler._drying_in_progress = {1: time.monotonic()}
|
|
|
|
|
+
|
|
|
|
|
+ state = MagicMock()
|
|
|
|
|
+ state.raw_data = {"ams": [{"id": 0, "dry_time": 120}]}
|
|
|
|
|
+ mock_pm.get_status.return_value = state
|
|
|
|
|
+
|
|
|
|
|
+ db = AsyncMock()
|
|
|
|
|
+ settings_returns = {
|
|
|
|
|
+ "queue_drying_enabled": self._make_setting("false"),
|
|
|
|
|
+ "ambient_drying_enabled": self._make_setting("false"),
|
|
|
|
|
+ }
|
|
|
|
|
+ db.execute = AsyncMock(side_effect=self._make_db_side_effect(settings_returns))
|
|
|
|
|
+
|
|
|
|
|
+ await scheduler._check_auto_drying(db, [], set())
|
|
|
|
|
+
|
|
|
|
|
+ assert mock_pm.send_drying_command.called
|
|
|
|
|
+ assert not scheduler._drying_in_progress
|
|
|
|
|
+
|
|
|
|
|
+ @pytest.mark.asyncio
|
|
|
|
|
+ @patch("backend.app.services.print_scheduler.printer_manager")
|
|
|
|
|
+ @patch("backend.app.services.print_scheduler.supports_drying", return_value=True)
|
|
|
|
|
+ async def test_ambient_continues_when_queue_empty(self, mock_sd, mock_pm, scheduler):
|
|
|
|
|
+ """Ambient drying continues even when queue has no scheduled items (unlike queue mode)."""
|
|
|
|
|
+ scheduler._drying_in_progress = {1: time.monotonic() - 100}
|
|
|
|
|
+
|
|
|
|
|
+ state = MagicMock()
|
|
|
|
|
+ state.raw_data = {
|
|
|
|
|
+ "ams": [
|
|
|
|
|
+ {
|
|
|
|
|
+ "id": 0,
|
|
|
|
|
+ "module_type": "n3f",
|
|
|
|
|
+ "dry_time": 600,
|
|
|
|
|
+ "humidity_raw": "75",
|
|
|
|
|
+ "dry_sf_reason": [],
|
|
|
|
|
+ "tray": [{"tray_type": "PLA"}],
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+ }
|
|
|
|
|
+ state.firmware_version = "01.09.00.00"
|
|
|
|
|
+ mock_pm.get_status.return_value = state
|
|
|
|
|
+ mock_pm.is_connected.return_value = True
|
|
|
|
|
+ mock_pm.get_model.return_value = "X1C"
|
|
|
|
|
+
|
|
|
|
|
+ scheduler._is_printer_idle = MagicMock(return_value=True)
|
|
|
|
|
+ db = AsyncMock()
|
|
|
|
|
+
|
|
|
|
|
+ settings_returns = {
|
|
|
|
|
+ "queue_drying_enabled": self._make_setting("false"),
|
|
|
|
|
+ "ambient_drying_enabled": self._make_setting("true"),
|
|
|
|
|
+ "ams_humidity_fair": self._make_setting("60"),
|
|
|
|
|
+ "queue_drying_block": self._make_setting("false"),
|
|
|
|
|
+ "drying_presets": None,
|
|
|
|
|
+ }
|
|
|
|
|
+ db.execute = AsyncMock(side_effect=self._make_db_side_effect(settings_returns))
|
|
|
|
|
+
|
|
|
|
|
+ await scheduler._check_auto_drying(db, [], set())
|
|
|
|
|
+
|
|
|
|
|
+ # Should NOT have sent stop — humidity still high, drying continues
|
|
|
|
|
+ for call in mock_pm.send_drying_command.call_args_list:
|
|
|
|
|
+ assert call.kwargs.get("mode") != 0, "Should not stop drying in ambient mode with high humidity"
|
|
|
|
|
+ assert 1 in scheduler._drying_in_progress
|
|
|
|
|
+
|
|
|
|
|
+ @pytest.mark.asyncio
|
|
|
|
|
+ @patch("backend.app.services.print_scheduler.printer_manager")
|
|
|
|
|
+ @patch("backend.app.services.print_scheduler.supports_drying", return_value=True)
|
|
|
|
|
+ async def test_queue_only_does_not_dry_without_scheduled_items(self, mock_sd, mock_pm, scheduler):
|
|
|
|
|
+ """Queue mode alone does NOT dry printers that have no scheduled queue items."""
|
|
|
|
|
+ state = MagicMock()
|
|
|
|
|
+ state.raw_data = {
|
|
|
|
|
+ "ams": [
|
|
|
|
|
+ {
|
|
|
|
|
+ "id": 0,
|
|
|
|
|
+ "module_type": "n3f",
|
|
|
|
|
+ "dry_time": 0,
|
|
|
|
|
+ "humidity_raw": "75",
|
|
|
|
|
+ "dry_sf_reason": [],
|
|
|
|
|
+ "tray": [{"tray_type": "PLA"}],
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+ }
|
|
|
|
|
+ state.firmware_version = "01.09.00.00"
|
|
|
|
|
+ mock_pm.get_status.return_value = state
|
|
|
|
|
+ mock_pm.is_connected.return_value = True
|
|
|
|
|
+ mock_pm.get_model.return_value = "X1C"
|
|
|
|
|
+
|
|
|
|
|
+ scheduler._is_printer_idle = MagicMock(return_value=True)
|
|
|
|
|
+ db = AsyncMock()
|
|
|
|
|
+
|
|
|
|
|
+ settings_returns = {
|
|
|
|
|
+ "queue_drying_enabled": self._make_setting("true"),
|
|
|
|
|
+ "ambient_drying_enabled": self._make_setting("false"),
|
|
|
|
|
+ "ams_humidity_fair": self._make_setting("60"),
|
|
|
|
|
+ "queue_drying_block": self._make_setting("false"),
|
|
|
|
|
+ "drying_presets": None,
|
|
|
|
|
+ }
|
|
|
|
|
+ db.execute = AsyncMock(side_effect=self._make_db_side_effect(settings_returns))
|
|
|
|
|
+
|
|
|
|
|
+ # No queue items at all
|
|
|
|
|
+ await scheduler._check_auto_drying(db, [], set())
|
|
|
|
|
+
|
|
|
|
|
+ mock_pm.send_drying_command.assert_not_called()
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+class TestBlockForDryingBugFix(_DryingTestBase):
|
|
|
|
|
+ """Regression: block mode should not skip humidity auto-stop for already-drying printers."""
|
|
|
|
|
+
|
|
|
|
|
+ @pytest.fixture
|
|
|
|
|
+ def scheduler(self):
|
|
|
|
|
+ s = PrintScheduler()
|
|
|
|
|
+ s._min_drying_seconds = 1800
|
|
|
|
|
+ return s
|
|
|
|
|
+
|
|
|
|
|
+ @pytest.mark.asyncio
|
|
|
|
|
+ @patch("backend.app.services.print_scheduler.printer_manager")
|
|
|
|
|
+ @patch("backend.app.services.print_scheduler.supports_drying", return_value=True)
|
|
|
|
|
+ async def test_block_mode_allows_humidity_stop_for_active_drying(self, mock_sd, mock_pm, scheduler):
|
|
|
|
|
+ """Bug fix: printer already drying in block mode should still check humidity to auto-stop."""
|
|
|
|
|
+ # Drying started 35 minutes ago
|
|
|
|
|
+ scheduler._drying_in_progress = {1: time.monotonic() - 2100}
|
|
|
|
|
+
|
|
|
|
|
+ state = MagicMock()
|
|
|
|
|
+ state.raw_data = {
|
|
|
|
|
+ "ams": [
|
|
|
|
|
+ {
|
|
|
|
|
+ "id": 0,
|
|
|
|
|
+ "module_type": "n3f",
|
|
|
|
|
+ "dry_time": 600,
|
|
|
|
|
+ "humidity_raw": "30", # Below threshold
|
|
|
|
|
+ "dry_sf_reason": [],
|
|
|
|
|
+ "tray": [{"tray_type": "PLA"}],
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+ }
|
|
|
|
|
+ state.firmware_version = "01.09.00.00"
|
|
|
|
|
+ mock_pm.get_status.return_value = state
|
|
|
|
|
+ mock_pm.is_connected.return_value = True
|
|
|
|
|
+ mock_pm.get_model.return_value = "X1C"
|
|
|
|
|
+
|
|
|
|
|
+ scheduler._is_printer_idle = MagicMock(return_value=True)
|
|
|
|
|
+ db = AsyncMock()
|
|
|
|
|
+
|
|
|
|
|
+ settings_returns = {
|
|
|
|
|
+ "queue_drying_enabled": self._make_setting("true"),
|
|
|
|
|
+ "ambient_drying_enabled": self._make_setting("false"),
|
|
|
|
|
+ "ams_humidity_fair": self._make_setting("60"),
|
|
|
|
|
+ "queue_drying_block": self._make_setting("true"),
|
|
|
|
|
+ "drying_presets": None,
|
|
|
|
|
+ }
|
|
|
|
|
+ db.execute = AsyncMock(side_effect=self._make_db_side_effect(settings_returns))
|
|
|
|
|
+
|
|
|
|
|
+ # Queue item exists for this printer (triggers block mode gate)
|
|
|
|
|
+ item = MagicMock()
|
|
|
|
|
+ item.printer_id = 1
|
|
|
|
|
+ item.scheduled_time = MagicMock()
|
|
|
|
|
+ item.manual_start = False
|
|
|
|
|
+
|
|
|
|
|
+ await scheduler._check_auto_drying(db, [item], set())
|
|
|
|
|
+
|
|
|
|
|
+ # Should have sent stop command — humidity dropped below threshold after 30+ min
|
|
|
|
|
+ mock_pm.send_drying_command.assert_any_call(1, 0, temp=0, duration=0, mode=0)
|
|
|
|
|
+
|
|
|
|
|
+ @pytest.mark.asyncio
|
|
|
|
|
+ @patch("backend.app.services.print_scheduler.printer_manager")
|
|
|
|
|
+ @patch("backend.app.services.print_scheduler.supports_drying", return_value=True)
|
|
|
|
|
+ async def test_block_mode_prevents_new_drying_start(self, mock_sd, mock_pm, scheduler):
|
|
|
|
|
+ """Block mode should still prevent starting NEW drying on printers with pending items."""
|
|
|
|
|
+ state = MagicMock()
|
|
|
|
|
+ state.raw_data = {
|
|
|
|
|
+ "ams": [
|
|
|
|
|
+ {
|
|
|
|
|
+ "id": 0,
|
|
|
|
|
+ "module_type": "n3f",
|
|
|
|
|
+ "dry_time": 0,
|
|
|
|
|
+ "humidity_raw": "75",
|
|
|
|
|
+ "dry_sf_reason": [],
|
|
|
|
|
+ "tray": [{"tray_type": "PLA"}],
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+ }
|
|
|
|
|
+ state.firmware_version = "01.09.00.00"
|
|
|
|
|
+ mock_pm.get_status.return_value = state
|
|
|
|
|
+ mock_pm.is_connected.return_value = True
|
|
|
|
|
+ mock_pm.get_model.return_value = "X1C"
|
|
|
|
|
+
|
|
|
|
|
+ scheduler._is_printer_idle = MagicMock(return_value=True)
|
|
|
|
|
+ db = AsyncMock()
|
|
|
|
|
+
|
|
|
|
|
+ settings_returns = {
|
|
|
|
|
+ "queue_drying_enabled": self._make_setting("true"),
|
|
|
|
|
+ "ambient_drying_enabled": self._make_setting("false"),
|
|
|
|
|
+ "ams_humidity_fair": self._make_setting("60"),
|
|
|
|
|
+ "queue_drying_block": self._make_setting("true"),
|
|
|
|
|
+ "drying_presets": None,
|
|
|
|
|
+ }
|
|
|
|
|
+ db.execute = AsyncMock(side_effect=self._make_db_side_effect(settings_returns))
|
|
|
|
|
+
|
|
|
|
|
+ item = MagicMock()
|
|
|
|
|
+ item.printer_id = 1
|
|
|
|
|
+ item.scheduled_time = MagicMock()
|
|
|
|
|
+ item.manual_start = False
|
|
|
|
|
+
|
|
|
|
|
+ await scheduler._check_auto_drying(db, [item], set())
|
|
|
|
|
+
|
|
|
|
|
+ # Should NOT start drying — block mode with pending items
|
|
|
|
|
+ mock_pm.send_drying_command.assert_not_called()
|