From 881761ff733953d280f10fe61b017852d6e4224a Mon Sep 17 00:00:00 2001 From: "Abe Diaz (@abe238)" Date: Tue, 26 May 2026 02:10:40 -0700 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20bambu=20doctor=20=E2=80=94=20firmwa?= =?UTF-8?q?re=20+=20library=20diagnostic=20report=20(#16)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prints versions, firmware, model, redacted serial suffix, MQTT/camera sanity checks, and matching entries from KNOWN_FIRMWARE_ISSUES. 7 new tests, 151 total. Closes #16. --- completions/_bambu | 1 + completions/bambu.bash | 2 +- src/bambu_ai/cli.py | 58 +++++++++++++++++++++++ src/bambu_ai/doctor.py | 105 +++++++++++++++++++++++++++++++++++++++++ tests/test_doctor.py | 105 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 270 insertions(+), 1 deletion(-) create mode 100644 src/bambu_ai/doctor.py create mode 100644 tests/test_doctor.py diff --git a/completions/_bambu b/completions/_bambu index 8e52796..47affa5 100644 --- a/completions/_bambu +++ b/completions/_bambu @@ -18,6 +18,7 @@ _bambu() { 'auto-off:fire a hook on FINISH/FAILED' 'queue:persistent print queue (add/list/clear/start)' 'cal:per-filament calibration tracking' + 'doctor:firmware + library diagnostic report' 'snap:capture one frame from the chamber camera' 'stream:capture frames periodically into a folder' 'timelapse:stitch captured frames into MP4' diff --git a/completions/bambu.bash b/completions/bambu.bash index 2060883..1e63254 100644 --- a/completions/bambu.bash +++ b/completions/bambu.bash @@ -12,7 +12,7 @@ _bambu() { } if [ "${cword}" -eq 1 ]; then - COMPREPLY=( $(compgen -W "status pause resume cancel home light print queue cal schedule quiet auto-off snap stream timelapse vision" -- "${cur}") ) + COMPREPLY=( $(compgen -W "status pause resume cancel home light print queue cal doctor schedule quiet auto-off snap stream timelapse vision" -- "${cur}") ) return fi diff --git a/src/bambu_ai/cli.py b/src/bambu_ai/cli.py index 2cccdf6..83a40ab 100644 --- a/src/bambu_ai/cli.py +++ b/src/bambu_ai/cli.py @@ -87,6 +87,59 @@ def _cmd_light(p, args): # ----------------------------------------------------------------- new subcommands +@_with_printer +def _cmd_doctor(p, _args) -> None: + """Diagnostic report (issue #16).""" + import sys as _sys + from importlib.metadata import PackageNotFoundError + from importlib.metadata import version as _pkg_version + + import bambu_ai + + from .doctor import build_report + + # Wait briefly for the printer state to populate. + for _ in range(50): + if p.get_state() not in ("UNKNOWN", None, ""): + break + time.sleep(0.1) + + cfg = load_config() + # Best-effort firmware discovery — bambulabs_api may or may not expose it. + firmware = None + for attr in ("get_firmware_version", "firmware_version"): + fn = getattr(p, attr, None) + if callable(fn): + try: + firmware = str(fn()) + except Exception: # noqa: BLE001 + pass + elif fn is not None: + firmware = str(fn) + if firmware: + break + + def _v(pkg: str) -> str: + try: + return _pkg_version(pkg) + except PackageNotFoundError: + return "?" + + # Sneaky: tell build_report whether MQTT is ready (without forging the printer object). + p._mqtt_ready_for_doctor = p.mqtt_client_ready() + report = build_report( + printer=p, + firmware=firmware, + model="Bambu Lab A1 mini", # this repo targets A1 mini explicitly + serial=cfg["SERIAL"], + bambu_ai_version=bambu_ai.__version__, + bambulabs_api_version=_v("bambulabs_api"), + paho_mqtt_version=_v("paho-mqtt"), + python_version=f"{_sys.version_info.major}.{_sys.version_info.minor}.{_sys.version_info.micro}", + ) + print(report.render()) + + def _cmd_cal_log(args) -> None: """Record calibration values for a spool (issue #7).""" from .calibration import CalibrationLog, CalibrationRecord @@ -555,6 +608,11 @@ def main() -> None: ) autooff.set_defaults(func=_cmd_autooff) + # doctor (issue #16) — diagnostic report + sub.add_parser("doctor", help="firmware + library + known-bug diagnostic report").set_defaults( + func=_cmd_doctor + ) + # cal (issue #7) — per-filament calibration tracking cal = sub.add_parser("cal", help="track per-filament calibration values") csub = cal.add_subparsers(dest="cal_cmd", required=True) diff --git a/src/bambu_ai/doctor.py b/src/bambu_ai/doctor.py new file mode 100644 index 0000000..5fbeec5 --- /dev/null +++ b/src/bambu_ai/doctor.py @@ -0,0 +1,105 @@ +"""bambu doctor — diagnostic report (issue #16). + +Prints firmware version, library versions, model, and any known +compatibility issues from a hand-maintained matrix. The goal is to save +users (and agents) from filing 'feature X broke after the firmware update' +issues without first checking whether the breakage is known. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +# Hand-maintained: known broken / quirky features by Bambu A1 / A1 mini firmware. +# Format: each entry is (firmware-prefix, feature, severity, description). +# severity ∈ {"info", "warn", "broken"}. +KNOWN_FIRMWARE_ISSUES: list[tuple[str, str, str, str]] = [ + ( + "01.07", + "ha-bambulab integration", + "warn", + "A1 + AMS2 sometimes detected as X1 — see ha-bambulab#1621", + ), + ( + "01.08", + "Access Code", + "info", + "Some firmware bumps regenerate the Access Code; if MQTT auth fails after update, re-grab it from the WLAN screen.", + ), +] + + +@dataclass +class DoctorReport: + """Structured report so callers (and tests) can inspect the result.""" + + firmware: str | None + model: str | None + serial_suffix: str # last 4 chars only, redacted + bambulabs_api_version: str + bambu_ai_version: str + paho_mqtt_version: str + python_version: str + known_issues: list[tuple[str, str, str]] # (feature, severity, description) + mqtt_ready: bool + camera_alive: bool | None + + def render(self) -> str: + lines: list[str] = ["bambu doctor — diagnostic report", ""] + lines.append("environment:") + lines.append(f" python: {self.python_version}") + lines.append(f" bambu-ai: {self.bambu_ai_version}") + lines.append(f" bambulabs_api: {self.bambulabs_api_version}") + lines.append(f" paho-mqtt: {self.paho_mqtt_version}") + lines.append("") + lines.append("printer:") + lines.append(f" firmware: {self.firmware or '?'}") + lines.append(f" model: {self.model or '?'}") + lines.append(f" serial suffix: …{self.serial_suffix}") + lines.append(f" MQTT ready: {self.mqtt_ready}") + lines.append(f" camera alive: {self.camera_alive}") + lines.append("") + if self.known_issues: + lines.append(f"known issues affecting firmware {self.firmware}:") + for feature, severity, desc in self.known_issues: + lines.append(f" [{severity.upper()}] {feature}: {desc}") + else: + lines.append("no known issues recorded for this firmware.") + return "\n".join(lines) + + +def match_known_issues(firmware: str | None) -> list[tuple[str, str, str]]: + if not firmware: + return [] + return [ + (feature, severity, desc) + for prefix, feature, severity, desc in KNOWN_FIRMWARE_ISSUES + if firmware.startswith(prefix) + ] + + +def build_report( + *, + printer: Any, + firmware: str | None, + model: str | None, + serial: str, + bambu_ai_version: str, + bambulabs_api_version: str, + paho_mqtt_version: str, + python_version: str, +) -> DoctorReport: + """Pure constructor — tests pass in stub values; the CLI fills them from the live printer.""" + return DoctorReport( + firmware=firmware, + model=model, + serial_suffix=serial[-4:] if serial else "????", + bambulabs_api_version=bambulabs_api_version, + bambu_ai_version=bambu_ai_version, + paho_mqtt_version=paho_mqtt_version, + python_version=python_version, + known_issues=match_known_issues(firmware), + mqtt_ready=bool(getattr(printer, "_mqtt_ready_for_doctor", None)) if printer is not None else False, + camera_alive=None, + ) diff --git a/tests/test_doctor.py b/tests/test_doctor.py new file mode 100644 index 0000000..80a3bd2 --- /dev/null +++ b/tests/test_doctor.py @@ -0,0 +1,105 @@ +"""Tests for the bambu doctor diagnostic builder (issue #16).""" + +from __future__ import annotations + +from unittest.mock import MagicMock + +from bambu_ai.doctor import build_report, match_known_issues + + +class TestMatchKnownIssues: + def test_returns_empty_when_no_firmware(self) -> None: + assert match_known_issues(None) == [] + assert match_known_issues("") == [] + + def test_matches_by_prefix(self) -> None: + issues = match_known_issues("01.07.04.00") + assert any("ha-bambulab" in feature.lower() for feature, _, _ in issues) + + def test_no_match_returns_empty(self) -> None: + # Hypothetical future firmware not in the table. + assert match_known_issues("99.99.99.99") == [] + + +class TestBuildReport: + def test_redacts_serial_to_last_4(self) -> None: + printer = MagicMock() + printer._mqtt_ready_for_doctor = True + r = build_report( + printer=printer, + firmware="01.07.01.00", + model="A1 mini", + serial="9999XXXXXXXXX20", + bambu_ai_version="0.1.0", + bambulabs_api_version="2.6.6", + paho_mqtt_version="2.1.0", + python_version="3.14.0", + ) + assert r.serial_suffix == "1720" + assert "9999XXXXXXXXX20" not in r.render() + + def test_renders_known_issues_section(self) -> None: + printer = MagicMock() + printer._mqtt_ready_for_doctor = True + r = build_report( + printer=printer, + firmware="01.07.01.00", + model="A1 mini", + serial="0000000000000XX", + bambu_ai_version="0.1.0", + bambulabs_api_version="2.6.6", + paho_mqtt_version="2.1.0", + python_version="3.14.0", + ) + out = r.render() + assert "known issues" in out.lower() + assert "ha-bambulab" in out + + def test_renders_no_issues_section_when_clean(self) -> None: + printer = MagicMock() + printer._mqtt_ready_for_doctor = True + r = build_report( + printer=printer, + firmware="99.99.99.99", + model="A1 mini", + serial="abcdefghijklmnop", + bambu_ai_version="0.1.0", + bambulabs_api_version="2.6.6", + paho_mqtt_version="2.1.0", + python_version="3.14.0", + ) + assert "no known issues" in r.render().lower() + + def test_render_includes_versions(self) -> None: + printer = MagicMock() + printer._mqtt_ready_for_doctor = True + out = build_report( + printer=printer, + firmware="01.07.01.00", + model="A1 mini", + serial="xxxxxxxxxxxxxxx0", + bambu_ai_version="0.1.0", + bambulabs_api_version="2.6.6", + paho_mqtt_version="2.1.0", + python_version="3.14.0", + ).render() + assert "0.1.0" in out + assert "2.6.6" in out + assert "2.1.0" in out + assert "3.14.0" in out + + def test_handles_missing_firmware_gracefully(self) -> None: + printer = MagicMock() + printer._mqtt_ready_for_doctor = False + r = build_report( + printer=printer, + firmware=None, + model=None, + serial="x", + bambu_ai_version="0.1.0", + bambulabs_api_version="2.6.6", + paho_mqtt_version="2.1.0", + python_version="3.14.0", + ) + out = r.render() + assert "?" in out # both firmware and model rendered as "?" From 301c876f9b0755f0474bd8226106722716e69c08 Mon Sep 17 00:00:00 2001 From: "Abe Diaz (@abe238)" Date: Tue, 26 May 2026 02:11:04 -0700 Subject: [PATCH 2/2] fix test_doctor: serial suffix should be 'XX20' not '1720' after rename --- tests/test_doctor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_doctor.py b/tests/test_doctor.py index 80a3bd2..85ccf67 100644 --- a/tests/test_doctor.py +++ b/tests/test_doctor.py @@ -35,7 +35,7 @@ def test_redacts_serial_to_last_4(self) -> None: paho_mqtt_version="2.1.0", python_version="3.14.0", ) - assert r.serial_suffix == "1720" + assert r.serial_suffix == "XX20" assert "9999XXXXXXXXX20" not in r.render() def test_renders_known_issues_section(self) -> None: