From 58ebdf518e83088220974f07be63ff3966eee7e1 Mon Sep 17 00:00:00 2001 From: RNL Date: Sat, 20 Jun 2026 14:48:45 +1000 Subject: [PATCH] Always show device dropdown and add bambu_diagnose for multi-printer troubleshooting. --- bambu_run/diagnostics.py | 78 +++++++++++ .../management/commands/bambu_diagnose.py | 128 ++++++++++++++++++ .../bambu_run/printer_dashboard.html | 5 +- bambu_run/views.py | 3 +- tests/test_diagnostics.py | 68 ++++++++++ tests/test_printer_routing.py | 9 ++ 6 files changed, 288 insertions(+), 3 deletions(-) create mode 100644 bambu_run/diagnostics.py create mode 100644 bambu_run/management/commands/bambu_diagnose.py create mode 100644 tests/test_diagnostics.py diff --git a/bambu_run/diagnostics.py b/bambu_run/diagnostics.py new file mode 100644 index 0000000..266c2c7 --- /dev/null +++ b/bambu_run/diagnostics.py @@ -0,0 +1,78 @@ +""" +Pure helpers for the `bambu_diagnose` management command. + +Kept separate from the command itself (and free of Django/network imports) +so the report-building and redaction logic can be unit-tested without +talking to the real Bambu Lab cloud or MQTT broker. +""" + +from datetime import datetime, timezone +from typing import Any, Dict, List, Optional + +# Keys whose values are always replaced outright, regardless of nesting depth. +_SECRET_KEY_SUBSTRINGS = ("password", "token", "secret", "access_code", "authorization") + +# Keys that identify a specific physical device/spool/account — not secret, +# but identifying, so they're partially masked by default before anything +# gets pasted into a public GitHub issue. +_IDENTIFIER_KEYS = {"dev_id", "device_id", "serial_number", "tray_uuid", "tag_uid", "uid"} + + +def _mask_identifier(value: Any) -> Any: + if not isinstance(value, str) or len(value) <= 8: + return "***" + return f"{value[:4]}...{value[-4:]}" + + +def redact_diagnostics(data: Any, redact: bool = True) -> Any: + """Recursively redact secrets and mask identifiers in a diagnostics payload. + + `redact=False` returns the data unchanged — only for the reporter's own + local debugging, never for anything posted publicly. + """ + if not redact: + return data + return _redact(data) + + +def _redact(obj: Any) -> Any: + if isinstance(obj, dict): + result = {} + for key, value in obj.items(): + lowered = key.lower() + if any(secret in lowered for secret in _SECRET_KEY_SUBSTRINGS): + result[key] = "***REDACTED***" + elif lowered in _IDENTIFIER_KEYS: + result[key] = _mask_identifier(value) + else: + result[key] = _redact(value) + return result + if isinstance(obj, list): + return [_redact(item) for item in obj] + return obj + + +def build_diagnostics_report( + devices: List[Dict[str, Any]], + raw_payloads: Dict[str, Optional[Dict[str, Any]]], +) -> Dict[str, Any]: + """Assemble the (pre-redaction) diagnostics report from discovered devices + and whatever raw MQTT payload was captured for each during the listen window. + """ + device_entries = [] + for device in devices: + dev_id = device.get("dev_id") + payload = raw_payloads.get(dev_id) + entry = { + "device_info": device, + "raw_mqtt_payload": payload, + } + if payload is None: + entry["note"] = "No MQTT data received within the listen window." + device_entries.append(entry) + + return { + "generated_at": datetime.now(timezone.utc).isoformat(), + "device_count": len(devices), + "devices": device_entries, + } diff --git a/bambu_run/management/commands/bambu_diagnose.py b/bambu_run/management/commands/bambu_diagnose.py new file mode 100644 index 0000000..fc18c46 --- /dev/null +++ b/bambu_run/management/commands/bambu_diagnose.py @@ -0,0 +1,128 @@ +""" +Diagnose multi-printer cloud data for a Bambu Lab account. + +Run this if `bambu_collector` doesn't pick up all your printers, or the data +collected for a second/third printer looks wrong. It authenticates with your +Bambu Lab account, lists every device the cloud API reports, listens briefly +for raw MQTT data from each one, and writes a redacted JSON report you can +attach to a GitHub issue. + +Usage: + python manage.py bambu_diagnose + python manage.py bambu_diagnose --listen-seconds 15 + python manage.py bambu_diagnose --output my_report.json + python manage.py bambu_diagnose --no-redact # local debugging only — do NOT post this output publicly +""" + +import json +import logging +import time + +from django.core.management.base import BaseCommand, CommandError + +from bambu_run.diagnostics import build_diagnostics_report, redact_diagnostics + +logger = logging.getLogger("bambu_run.diagnose") + + +class Command(BaseCommand): + help = "Authenticate, list every printer on the account, and write a redacted diagnostics report." + + def add_arguments(self, parser): + parser.add_argument( + "--listen-seconds", type=float, default=8.0, + help="How long to listen for MQTT data per device (default: 8)", + ) + parser.add_argument( + "--output", type=str, default=None, + help="Output file path (default: bambu_diagnostics_.json)", + ) + parser.add_argument( + "--no-redact", action="store_true", + help="Keep full serials/identifiers unmasked. For your own debugging only — " + "do not paste this output into a public GitHub issue.", + ) + + def handle(self, *args, **options): + import os + from bambu_run.mqtt_client import BambuPrinter, BambuClient + + listen_seconds = options["listen_seconds"] + redact = not options["no_redact"] + + bambu_username = os.environ.get("BAMBU_USERNAME") + bambu_password = os.environ.get("BAMBU_PASSWORD") + bambu_token = os.environ.get("BAMBU_TOKEN") + + if not bambu_token and not all([bambu_username, bambu_password]): + raise CommandError( + "Either BAMBU_TOKEN or both BAMBU_USERNAME and BAMBU_PASSWORD " + "environment variables must be set" + ) + + self.stdout.write("Authenticating with Bambu Lab cloud...") + auth = BambuPrinter(username=bambu_username, password=bambu_password, token=bambu_token) + token = auth._ensure_token() + + cloud = BambuClient(token=token) + devices = cloud.get_devices() + + self.stdout.write(self.style.SUCCESS(f"Found {len(devices)} device(s) on this account:")) + for device in devices: + self.stdout.write( + f" - {device.get('name', 'unknown')} " + f"({device.get('dev_product_name', 'unknown model')}) " + f"online={device.get('online')}" + ) + if len(devices) < 2: + self.stdout.write(self.style.WARNING( + "Only one device returned by the cloud API — if you own multiple printers, " + "this is likely the root cause. Note this in the GitHub issue." + )) + + raw_payloads = {} + for device in devices: + dev_id = device.get("dev_id") + if not dev_id: + continue + self.stdout.write(f"Listening to {device.get('name', dev_id)} for {listen_seconds:.0f}s...") + client = BambuPrinter(token=token, device_id=dev_id) + try: + client.connect(blocking=False) + self._request_full_status_when_ready(client) + time.sleep(listen_seconds) + state = client.get_state() + raw_payloads[dev_id] = state._raw_data.get("print") if state._raw_data else None + except Exception as e: + self.stdout.write(self.style.WARNING(f" Could not collect data for {dev_id}: {e}")) + raw_payloads[dev_id] = None + finally: + client.disconnect() + + report = build_diagnostics_report(devices, raw_payloads) + report = redact_diagnostics(report, redact=redact) + + output_path = options["output"] or f"bambu_diagnostics_{int(time.time())}.json" + with open(output_path, "w") as f: + json.dump(report, f, indent=2, default=str) + + self.stdout.write(self.style.SUCCESS(f"\nDiagnostics written to: {output_path}")) + if not redact: + self.stdout.write(self.style.WARNING( + "--no-redact was used: this file contains unmasked serials/identifiers. " + "Do not attach it to a public GitHub issue as-is." + )) + else: + self.stdout.write( + "Serials/identifiers are masked. Please skim the file once before posting — " + "then attach it to https://github.com/RunLit/Bambu-Run/issues/10" + ) + + def _request_full_status_when_ready(self, client, timeout: float = 20.0) -> None: + deadline = time.time() + timeout + while time.time() < deadline: + mqtt_client = getattr(client, "_mqtt", None) + if mqtt_client is not None and getattr(mqtt_client, "connected", False): + client._mqtt.request_full_status() + return + time.sleep(0.5) diff --git a/bambu_run/templates/bambu_run/printer_dashboard.html b/bambu_run/templates/bambu_run/printer_dashboard.html index 0c88400..4837c19 100644 --- a/bambu_run/templates/bambu_run/printer_dashboard.html +++ b/bambu_run/templates/bambu_run/printer_dashboard.html @@ -15,8 +15,9 @@

{% if show_printer_switcher %} -
- {% for p in all_printers %}