mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-18 11:56:01 +02:00
Fix AMS slot changes failing until manual reconnect (#887)
Add timeout and retry to the developer mode probe. After a keep-alive timeout, paho auto-reconnects but the session can be half-broken: the printer sends status but ignores commands. The probe had no recovery — one unanswered probe permanently blocked retries. Now times out after 10s with one retry; two consecutive failures force-close the socket for a clean reconnect.
This commit is contained in:
@@ -2788,6 +2788,147 @@ class TestDeveloperModeDetection:
|
||||
assert mqtt_client.state.developer_mode is False
|
||||
|
||||
|
||||
class TestDeveloperModeProbeTimeout:
|
||||
"""Tests for developer mode probe timeout, retry, and forced reconnect (#887).
|
||||
|
||||
When a printer's MQTT session is half-broken (sends status but ignores
|
||||
commands), the developer mode probe gets no response. The timeout logic
|
||||
retries once, then force-closes the socket on the second failure.
|
||||
"""
|
||||
|
||||
@pytest.fixture
|
||||
def mqtt_client(self):
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from backend.app.services.bambu_mqtt import BambuMQTTClient
|
||||
|
||||
client = BambuMQTTClient(
|
||||
ip_address="192.168.1.100",
|
||||
serial_number="TEST123",
|
||||
access_code="12345678",
|
||||
)
|
||||
# Simulate connected state with a mock MQTT client
|
||||
client.state.connected = True
|
||||
mock_paho = MagicMock()
|
||||
mock_paho.socket.return_value = MagicMock()
|
||||
client._client = mock_paho
|
||||
return client
|
||||
|
||||
def _make_pushall_data(self):
|
||||
"""Create a print data dict with >30 keys (triggers probe) and no 'fun' field."""
|
||||
return {f"key_{i}": i for i in range(35)}
|
||||
|
||||
def test_first_timeout_allows_retry(self, mqtt_client):
|
||||
"""After first probe timeout, _dev_mode_probed resets to allow retry."""
|
||||
import time
|
||||
|
||||
data = self._make_pushall_data()
|
||||
|
||||
# First pushall triggers the probe
|
||||
mqtt_client._update_state(data)
|
||||
assert mqtt_client._dev_mode_probed is True
|
||||
assert mqtt_client._dev_mode_probe_seq is not None
|
||||
assert mqtt_client.state.developer_mode is None
|
||||
|
||||
# Simulate 11 seconds passing
|
||||
mqtt_client._dev_mode_probe_time = time.monotonic() - 11.0
|
||||
|
||||
# Next status message detects the timeout
|
||||
mqtt_client._update_state(data)
|
||||
assert mqtt_client._dev_mode_probe_failures == 1
|
||||
assert mqtt_client._dev_mode_probe_seq is None
|
||||
# Should allow retry on next full message
|
||||
assert mqtt_client._dev_mode_probed is False
|
||||
# Connection should NOT be force-closed after 1 failure
|
||||
assert mqtt_client.state.connected is True
|
||||
|
||||
def test_second_timeout_forces_reconnect(self, mqtt_client):
|
||||
"""After two consecutive probe timeouts, force-close the socket."""
|
||||
import time
|
||||
|
||||
data = self._make_pushall_data()
|
||||
state_change_called = []
|
||||
mqtt_client.on_state_change = lambda s: state_change_called.append(True)
|
||||
|
||||
# First probe + timeout
|
||||
mqtt_client._update_state(data)
|
||||
mqtt_client._dev_mode_probe_time = time.monotonic() - 11.0
|
||||
mqtt_client._update_state(data)
|
||||
assert mqtt_client._dev_mode_probe_failures == 1
|
||||
|
||||
# Second probe (retry) + timeout
|
||||
mqtt_client._update_state(data) # triggers new probe
|
||||
assert mqtt_client._dev_mode_probed is True
|
||||
mqtt_client._dev_mode_probe_time = time.monotonic() - 11.0
|
||||
mqtt_client._update_state(data) # detects second timeout
|
||||
|
||||
assert mqtt_client._dev_mode_probe_failures == 2
|
||||
assert mqtt_client.state.connected is False
|
||||
assert mqtt_client._stale_reconnecting is True
|
||||
# Socket should have been closed
|
||||
mqtt_client._client.socket().close.assert_called()
|
||||
# on_state_change should have been called
|
||||
assert len(state_change_called) > 0
|
||||
|
||||
def test_successful_probe_resets_failure_counter(self, mqtt_client):
|
||||
"""A probe response after a previous failure resets the counter."""
|
||||
import time
|
||||
|
||||
data = self._make_pushall_data()
|
||||
|
||||
# First probe + timeout → failure=1
|
||||
mqtt_client._update_state(data)
|
||||
seq = mqtt_client._dev_mode_probe_seq
|
||||
mqtt_client._dev_mode_probe_time = time.monotonic() - 11.0
|
||||
mqtt_client._update_state(data)
|
||||
assert mqtt_client._dev_mode_probe_failures == 1
|
||||
|
||||
# Retry probe
|
||||
mqtt_client._update_state(data)
|
||||
new_seq = mqtt_client._dev_mode_probe_seq
|
||||
assert new_seq is not None
|
||||
assert new_seq != seq
|
||||
|
||||
# Simulate successful response
|
||||
mqtt_client._handle_dev_mode_probe_response(
|
||||
{
|
||||
"command": "ams_filament_setting",
|
||||
"sequence_id": new_seq,
|
||||
"result": "success",
|
||||
}
|
||||
)
|
||||
assert mqtt_client._dev_mode_probe_failures == 0
|
||||
assert mqtt_client.state.developer_mode is True
|
||||
assert mqtt_client._dev_mode_probe_seq is None
|
||||
|
||||
def test_no_timeout_when_probe_not_sent(self, mqtt_client):
|
||||
"""The timeout branch is only entered when a probe is pending."""
|
||||
# No probe sent — _dev_mode_probed is False, _dev_mode_probe_seq is None
|
||||
data = {"gcode_state": "IDLE", "mc_percent": 0} # < 30 keys
|
||||
mqtt_client._update_state(data)
|
||||
assert mqtt_client._dev_mode_probe_failures == 0
|
||||
|
||||
def test_on_connect_resets_probe_state(self, mqtt_client):
|
||||
"""_on_connect resets all probe-related state for fresh detection."""
|
||||
import time
|
||||
|
||||
mqtt_client._dev_mode_probed = True
|
||||
mqtt_client._dev_mode_probe_seq = "42"
|
||||
mqtt_client._dev_mode_probe_time = time.monotonic()
|
||||
mqtt_client._dev_mode_probe_failures = 2
|
||||
mqtt_client.state.developer_mode = True
|
||||
|
||||
# subscribe() must return (result, mid) tuple
|
||||
mqtt_client._client.subscribe.return_value = (0, 1)
|
||||
mqtt_client._on_connect(mqtt_client._client, None, None, 0)
|
||||
|
||||
assert mqtt_client.state.developer_mode is None
|
||||
assert mqtt_client._dev_mode_probed is False
|
||||
assert mqtt_client._dev_mode_probe_seq is None
|
||||
assert mqtt_client._dev_mode_probe_time == 0.0
|
||||
assert mqtt_client._dev_mode_probe_failures == 0
|
||||
|
||||
|
||||
class TestVtTrayNormalization:
|
||||
"""Tests for vt_tray dict→list normalization in _update_state.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user