diff --git a/bambu_run/forms.py b/bambu_run/forms.py index 0c81920..a69aeaf 100644 --- a/bambu_run/forms.py +++ b/bambu_run/forms.py @@ -55,7 +55,7 @@ class FilamentForm(forms.ModelForm): 'filament_type', 'type', 'sub_type', 'brand', 'color', 'color_hex', 'is_transparent', 'diameter', 'initial_weight_grams', 'remaining_percent', 'remaining_weight_grams', - 'is_loaded_in_ams', 'current_tray_id', + 'is_loaded_in_ams', 'current_tray_id', 'ams_unit_id', 'ams_type', 'purchase_date', 'purchase_price', 'supplier', 'notes' ] widgets = { @@ -87,7 +87,15 @@ class FilamentForm(forms.ModelForm): 'remaining_weight_grams': forms.NumberInput(attrs={'class': 'form-control', 'readonly': 'readonly'}), 'is_transparent': forms.CheckboxInput(attrs={'class': 'form-check-input', 'id': 'id_is_transparent'}), 'is_loaded_in_ams': forms.CheckboxInput(attrs={'class': 'form-check-input'}), - 'current_tray_id': forms.NumberInput(attrs={'class': 'form-control', 'min': '0', 'max': '3'}), + 'current_tray_id': forms.NumberInput(attrs={ + 'class': 'form-control', 'min': '0', 'max': '15', + 'placeholder': '0–3 for AMS / AMS 2 Pro, 0 for AMS HT', + }), + 'ams_unit_id': forms.NumberInput(attrs={ + 'class': 'form-control', 'min': '0', 'max': '255', + 'placeholder': 'AMS unit id (0,1,… or 128 for AMS HT)', + }), + 'ams_type': forms.Select(attrs={'class': 'form-select'}), 'purchase_date': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}), 'purchase_price': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}), 'supplier': forms.TextInput(attrs={'class': 'form-control'}), @@ -106,6 +114,8 @@ class FilamentForm(forms.ModelForm): self.fields['type'].required = False self.fields['sub_type'].required = False self.fields['brand'].required = False + self.fields['ams_unit_id'].required = False + self.fields['ams_type'].required = False self._populate_color_choices() diff --git a/bambu_run/management/commands/bambu_collector.py b/bambu_run/management/commands/bambu_collector.py index a6a0a2d..1b7f462 100644 --- a/bambu_run/management/commands/bambu_collector.py +++ b/bambu_run/management/commands/bambu_collector.py @@ -111,6 +111,8 @@ class Command(BaseCommand): try: if run_once: + import time as _time + _time.sleep(5) self._collect_printer_data() logger.info("Single collection completed successfully") else: @@ -122,6 +124,24 @@ class Command(BaseCommand): logger.exception(f"Fatal error in main loop: {e}") raise CommandError(f"Runner failed: {e}") + def _request_full_status_when_ready(self, timeout: float = 20.0) -> None: + """Send pushall once the MQTT broker connection is confirmed. + + BambuPrinter._connected is set True immediately after connect(blocking=False), + before the broker handshake. Poll MQTTClient.connected (set in _on_connect) + instead, so publish() won't raise "Not connected to broker". + """ + import time as _time + deadline = _time.time() + timeout + while _time.time() < deadline: + mqtt_client = getattr(self.printer_client, "_mqtt", None) + if mqtt_client is not None and getattr(mqtt_client, "connected", False): + self.printer_client._mqtt.request_full_status() + logger.info("Sent MQTT pushall request") + return + _time.sleep(0.5) + logger.warning("MQTT broker connection not confirmed within %.1fs; skipping pushall", timeout) + def _configure_logging(self): log_level = logging.DEBUG if self.verbose else logging.INFO logger.setLevel(log_level) @@ -167,6 +187,11 @@ class Command(BaseCommand): logger.info("Initiating MQTT connection...") self.printer_client.connect(blocking=False) logger.info("MQTT connection initiated (non-blocking)") + # Request full status so AMS + dual-nozzle data arrive on startup. + try: + self._request_full_status_when_ready() + except Exception as e: + logger.warning("pushall request skipped (non-fatal): %s", e) except Exception as e: if "CERTIFICATE_VERIFY_FAILED" in str(e) or "SSL" in str(e): @@ -377,6 +402,8 @@ class Command(BaseCommand): created_by='Auto Detection', is_loaded_in_ams=True, current_tray_id=tray_data.get('tray_id'), + ams_unit_id=tray_data.get('ams_unit_id'), + ams_type=tray_data.get('ams_type', '') or '', last_loaded_date=timezone.now(), ) @@ -390,9 +417,13 @@ class Command(BaseCommand): return filament - def _update_filament_status(self, filament, tray_id, remain_percent): + def _update_filament_status(self, filament, tray_id, remain_percent, tray_data=None): from bambu_run.models import Filament + tray_data = tray_data or {} + ams_unit_id = tray_data.get('ams_unit_id') + ams_type_label = tray_data.get('ams_type', '') or '' + if filament.remaining_percent != remain_percent: filament.remaining_percent = remain_percent filament.update_remaining_weight() @@ -400,10 +431,19 @@ class Command(BaseCommand): if self.verbose: logger.debug(f"Updated filament {filament}: {remain_percent}%") - if not filament.is_loaded_in_ams or filament.current_tray_id != tray_id: - previous_filament = Filament.objects.filter( + location_changed = ( + not filament.is_loaded_in_ams + or filament.current_tray_id != tray_id + or (ams_unit_id is not None and filament.ams_unit_id != ams_unit_id) + ) + if location_changed: + # Unload anything previously occupying THIS exact (unit, tray) slot. + unload_qs = Filament.objects.filter( is_loaded_in_ams=True, current_tray_id=tray_id - ).exclude(id=filament.id).first() + ).exclude(id=filament.id) + if ams_unit_id is not None: + unload_qs = unload_qs.filter(ams_unit_id=ams_unit_id) + previous_filament = unload_qs.first() if previous_filament: previous_filament.is_loaded_in_ams = False @@ -411,14 +451,21 @@ class Command(BaseCommand): previous_filament.save() logger.info( f"Auto-unloaded {previous_filament} from Tray {tray_id} " - f"(replaced by {filament.brand} {filament.type} - {filament.color})" + f"(unit {ams_unit_id}; replaced by {filament.brand} {filament.type} - {filament.color})" ) filament.is_loaded_in_ams = True filament.current_tray_id = tray_id + if ams_unit_id is not None: + filament.ams_unit_id = ams_unit_id + if ams_type_label: + filament.ams_type = ams_type_label filament.last_loaded_date = timezone.now() if self.verbose: - logger.debug(f"Updated filament location: Tray {tray_id}") + logger.debug(f"Updated filament location: unit={ams_unit_id} tray={tray_id}") + elif ams_type_label and filament.ams_type != ams_type_label: + # Same slot but ams_type was previously unknown — fill it in. + filament.ams_type = ams_type_label filament.save() @@ -439,10 +486,13 @@ class Command(BaseCommand): if filament: remain_percent = tray_data.get('remain_percent') if remain_percent is not None: - self._update_filament_status(filament, tray_id, remain_percent) + self._update_filament_status(filament, tray_id, remain_percent, tray_data) - unit_id = str(int(tray_id) // 4) if tray_id.isdigit() else None - unit_data = ams_units.get(unit_id, {}) + # Locate the AMS unit this tray belongs to. Use the unit_id supplied + # by the snapshot directly (matches MQTT ams[i].id, including 128 for AMS HT) + # — the legacy `tray_id // 4` math breaks for AMS HT. + unit_id_int = tray_data.get('ams_unit_id') + unit_data = ams_units.get(str(unit_id_int)) if unit_id_int is not None else {} FilamentSnapshot.objects.create( printer_metric=printer_metric, @@ -595,6 +645,10 @@ class Command(BaseCommand): chamber_temp=self._to_decimal(snapshot.get("chamber_temp")), nozzle_diameter=self._to_decimal(snapshot.get("nozzle_diameter")), nozzle_type=snapshot.get("nozzle_type"), + nozzle_temp_left=self._to_decimal(snapshot.get("nozzle_temp_left")), + nozzle_target_temp_left=self._to_decimal(snapshot.get("nozzle_target_temp_left")), + nozzle_diameter_left=self._to_decimal(snapshot.get("nozzle_diameter_left")), + nozzle_type_left=snapshot.get("nozzle_type_left"), gcode_state=snapshot.get("gcode_state"), print_type=snapshot.get("print_type"), print_percent=snapshot.get("print_percent"), diff --git a/bambu_run/migrations/0004_h2c_dual_nozzle_and_ams_fields.py b/bambu_run/migrations/0004_h2c_dual_nozzle_and_ams_fields.py new file mode 100644 index 0000000..e409272 --- /dev/null +++ b/bambu_run/migrations/0004_h2c_dual_nozzle_and_ams_fields.py @@ -0,0 +1,90 @@ +# Generated by Django 5.2.8 on 2026-05-07 04:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("bambu_run", "0003_cloud_task"), + ] + + operations = [ + migrations.AddField( + model_name="filament", + name="ams_type", + field=models.CharField( + blank=True, + choices=[ + ("AMS", "AMS"), + ("AMS 2 Pro", "AMS 2 Pro"), + ("AMS HT", "AMS HT"), + ], + default="", + help_text="Type of the AMS unit this spool is loaded in (AMS / AMS 2 Pro / AMS HT)", + max_length=32, + ), + ), + migrations.AddField( + model_name="filament", + name="ams_unit_id", + field=models.PositiveSmallIntegerField( + blank=True, + db_index=True, + help_text="Which physical AMS unit this spool is loaded in (matches MQTT ams[i].id; 128 = AMS HT)", + null=True, + ), + ), + migrations.AddField( + model_name="printermetrics", + name="nozzle_diameter_left", + field=models.DecimalField( + blank=True, + decimal_places=2, + help_text="Left nozzle diameter (mm). H2C only.", + max_digits=3, + null=True, + ), + ), + migrations.AddField( + model_name="printermetrics", + name="nozzle_target_temp_left", + field=models.DecimalField( + blank=True, + decimal_places=2, + help_text="Left extruder target temperature (°C). H2C only.", + max_digits=5, + null=True, + ), + ), + migrations.AddField( + model_name="printermetrics", + name="nozzle_temp_left", + field=models.DecimalField( + blank=True, + decimal_places=2, + help_text="Left extruder current temperature (°C). H2C only.", + max_digits=5, + null=True, + ), + ), + migrations.AddField( + model_name="printermetrics", + name="nozzle_type_left", + field=models.CharField( + blank=True, + help_text="Left nozzle type (e.g. HS01-0.4). H2C only.", + max_length=50, + null=True, + ), + ), + migrations.AlterField( + model_name="filament", + name="current_tray_id", + field=models.IntegerField( + blank=True, + help_text="Tray slot index within its AMS unit (0-3 for AMS/AMS 2 Pro, 0 for AMS HT)", + null=True, + ), + ), + ] diff --git a/bambu_run/models.py b/bambu_run/models.py index e6d8f26..cfa3ee1 100644 --- a/bambu_run/models.py +++ b/bambu_run/models.py @@ -2,6 +2,33 @@ from django.db import models from django.utils import timezone +# Bambu AMS model-code → human-readable type label. +# Source: live H2C MQTT probe — `print.ams.ams[i].info` field. +# Add new codes as they are observed (e.g. AMS Lite, future variants). +AMS_INFO_TO_TYPE = { + "1001": "AMS", + "1003": "AMS 2 Pro", + "2104": "AMS HT", +} + +AMS_TYPE_CHOICES = [ + ("AMS", "AMS"), + ("AMS 2 Pro", "AMS 2 Pro"), + ("AMS HT", "AMS HT"), +] + + +def ams_type_from_info(info_code) -> str: + """Resolve an AMS unit's `info` model code to a human label. + + The HT unit reports its `id` with the 0x80 bit set (e.g. 128) — when the info + code is unknown, that bit is a reasonable secondary hint for HT identification. + """ + if info_code is None: + return "" + return AMS_INFO_TO_TYPE.get(str(info_code), "") + + class Printer(models.Model): """Represents a Bambu Lab 3D printer device""" @@ -58,12 +85,32 @@ class PrinterMetrics(models.Model): max_digits=5, decimal_places=2, null=True, blank=True ) - # Nozzle info + # Nozzle info — single-nozzle / right-side back-compat fields. On dual-nozzle + # printers (H2C) these mirror the right extruder; the left extruder uses the + # `_left` columns below. nozzle_diameter = models.DecimalField( max_digits=3, decimal_places=2, null=True, blank=True ) nozzle_type = models.CharField(max_length=50, null=True, blank=True) + # H2C dual-nozzle: left-side fields (NULL on single-nozzle printers). + nozzle_temp_left = models.DecimalField( + max_digits=5, decimal_places=2, null=True, blank=True, + help_text="Left extruder current temperature (°C). H2C only." + ) + nozzle_target_temp_left = models.DecimalField( + max_digits=5, decimal_places=2, null=True, blank=True, + help_text="Left extruder target temperature (°C). H2C only." + ) + nozzle_diameter_left = models.DecimalField( + max_digits=3, decimal_places=2, null=True, blank=True, + help_text="Left nozzle diameter (mm). H2C only." + ) + nozzle_type_left = models.CharField( + max_length=50, null=True, blank=True, + help_text="Left nozzle type (e.g. HS01-0.4). H2C only." + ) + # Print job status gcode_state = models.CharField( max_length=50, null=True, blank=True, help_text="FINISH, RUNNING, IDLE, etc." @@ -365,7 +412,16 @@ class Filament(models.Model): ) current_tray_id = models.IntegerField( null=True, blank=True, - help_text="Which AMS slot (0-3) if loaded" + help_text="Tray slot index within its AMS unit (0-3 for AMS/AMS 2 Pro, 0 for AMS HT)" + ) + ams_unit_id = models.PositiveSmallIntegerField( + null=True, blank=True, db_index=True, + help_text="Which physical AMS unit this spool is loaded in (matches MQTT ams[i].id; 128 = AMS HT)" + ) + ams_type = models.CharField( + max_length=32, blank=True, default="", + choices=AMS_TYPE_CHOICES, + help_text="Type of the AMS unit this spool is loaded in (AMS / AMS 2 Pro / AMS HT)" ) last_loaded_date = models.DateTimeField( null=True, blank=True, diff --git a/bambu_run/mqtt_client.py b/bambu_run/mqtt_client.py index 9c3aa70..b451c98 100644 --- a/bambu_run/mqtt_client.py +++ b/bambu_run/mqtt_client.py @@ -335,10 +335,16 @@ class PrinterState: wifi_signal: str = "" wifi_signal_dbm: int = 0 - # Nozzle info + # Nozzle info — single-nozzle / right-side back-compat fields. nozzle_diameter: float = 0.4 nozzle_type: str = "" + # H2C dual-nozzle: left-side fields (None on single-nozzle printers). + nozzle_temp_left: Optional[float] = None + nozzle_target_temp_left: Optional[float] = None + nozzle_diameter_left: Optional[float] = None + nozzle_type_left: Optional[str] = None + # System status home_flag: int = 0 hw_switch_state: int = 0 @@ -410,6 +416,21 @@ class PrinterState: wifi_signal = print_data.get("wifi_signal", "") + # H2C dual-nozzle decoding. The H2C reports per-extruder temperatures + # under `print.device.extruder.info[]` as a 2-element array (index 0 = + # right, index 1 = left). The `temp` field is bit-packed: + # `temp_raw = (target << 16) | current`, both °C as ints. + nozzle_temp_left = None + nozzle_target_temp_left = None + device = print_data.get("device") or {} + extruders = (device.get("extruder") or {}).get("info") or [] + if len(extruders) >= 2: + left = extruders[1] + t = left.get("temp") + if isinstance(t, int): + nozzle_target_temp_left = float((t >> 16) & 0xFFFF) + nozzle_temp_left = float(t & 0xFFFF) + return cls( timestamp=timestamp, sequence_id=str(print_data.get("sequence_id", "")), @@ -438,6 +459,13 @@ class PrinterState: wifi_signal_dbm=cls._parse_wifi_signal(wifi_signal), nozzle_diameter=float(print_data.get("nozzle_diameter", 0.4)), nozzle_type=print_data.get("nozzle_type", ""), + nozzle_temp_left=nozzle_temp_left, + nozzle_target_temp_left=nozzle_target_temp_left, + # Diameter/type per side: H2C currently uses uniform nozzles, so reuse top-level + # values. If a future probe shows per-side diameter/type variance, plumb it from + # `device.nozzle.info[]` cross-referenced against `device.extruder.info[i].id`. + nozzle_diameter_left=float(print_data.get("nozzle_diameter", 0.4)) if nozzle_temp_left is not None else None, + nozzle_type_left=print_data.get("nozzle_type", "") if nozzle_temp_left is not None else None, home_flag=int(print_data.get("home_flag", 0)), hw_switch_state=int(print_data.get("hw_switch_state", 0)), mc_print_stage=str(print_data.get("mc_print_stage", "")), @@ -473,6 +501,14 @@ class PrinterState: "chamber_temp": round(self.chamber_temp, 2), "nozzle_diameter": self.nozzle_diameter, "nozzle_type": self.nozzle_type, + "nozzle_temp_left": ( + round(self.nozzle_temp_left, 2) if self.nozzle_temp_left is not None else None + ), + "nozzle_target_temp_left": ( + round(self.nozzle_target_temp_left, 2) if self.nozzle_target_temp_left is not None else None + ), + "nozzle_diameter_left": self.nozzle_diameter_left, + "nozzle_type_left": self.nozzle_type_left, "gcode_state": self.gcode_state, "print_type": self.print_type, "print_percent": self.print_percent, @@ -515,8 +551,19 @@ class PrinterState: snapshot["tray_now"] = self.ams.tray_now snapshot["ams_version"] = self.ams.version + from .models import ams_type_from_info + filaments = [] for unit in self.ams.units: + # `unit_id` is the AMS unit's own id from the MQTT payload — for the + # original AMS / AMS 2 Pro it's a small int (0,1,2,...); for AMS HT + # it has the 0x80 bit set (e.g. 128). Don't compute tray_id // 4 — + # multi-AMS-type setups are not contiguous. + try: + unit_id_int = int(unit.unit_id) + except (TypeError, ValueError): + unit_id_int = None + ams_type_label = ams_type_from_info(unit.info) for tray in unit.trays: if tray.tray_type: filaments.append({ @@ -542,6 +589,9 @@ class PrinterState: "tray_bed_temp": tray.tray_bed_temp, "bed_temp_type": tray.bed_temp_type, "cols": tray.cols, + "ams_unit_id": unit_id_int, + "ams_info": unit.info, + "ams_type": ams_type_label, }) snapshot["filaments"] = filaments @@ -552,6 +602,7 @@ class PrinterState: "ams_id": unit.ams_id, "chip_id": unit.chip_id, "info": unit.info, + "ams_type": ams_type_from_info(unit.info), "humidity": unit.humidity, "humidity_raw": unit.humidity_raw, "temp": unit.temp, diff --git a/bambu_run/static/bambu_run/js/printer_charts.js b/bambu_run/static/bambu_run/js/printer_charts.js index 857dfb1..a40a015 100644 --- a/bambu_run/static/bambu_run/js/printer_charts.js +++ b/bambu_run/static/bambu_run/js/printer_charts.js @@ -1,7 +1,7 @@ // 3D Printer Charts Initialization and Management // Chart.js implementation for printer metrics visualization -let nozzleTempChart, bedTempChart, printProgressChart, fanSpeedsChart; +let nozzleTempChart, nozzleTempLeftChart, bedTempChart, printProgressChart, fanSpeedsChart; let wifiSignalChart, amsConditionsChart, layerProgressChart, filamentTimelineChart; function showNoDataMessage(canvasId) { @@ -75,6 +75,50 @@ function initPrinterCharts(printerData, apiUrl) { options: getTemperatureChartOptions(tickColor, gridColor, '°C') }); + // Initialize Left Nozzle Temperature Chart (H2C-class dual-nozzle). + // Mounted only when the canvas exists AND the API returned non-null + // left-side samples — single-nozzle printers leave the column NULL. + const nozzleLeftCanvas = document.getElementById('nozzleTempLeftChart'); + const hasLeftData = Array.isArray(printerData.nozzle_temp_left) + && printerData.nozzle_temp_left.some(v => v !== null && v !== undefined); + if (nozzleLeftCanvas && hasLeftData) { + const nozzleLeftCtx = nozzleLeftCanvas.getContext('2d'); + nozzleTempLeftChart = new Chart(nozzleLeftCtx, { + type: 'line', + data: { + labels: printerData.timestamps, + datasets: [ + { + label: 'Actual Temp (Left)', + data: printerData.nozzle_temp_left, + borderColor: 'rgb(54, 162, 235)', + backgroundColor: 'rgba(54, 162, 235, 0.1)', + tension: 0.3, + borderWidth: 2, + pointRadius: 0, + pointHoverRadius: 3, + spanGaps: true + }, + { + label: 'Target Temp (Left)', + data: printerData.nozzle_target_temp_left, + borderColor: 'rgb(153, 102, 255)', + backgroundColor: 'rgba(153, 102, 255, 0.05)', + borderDash: [5, 5], + tension: 0.3, + borderWidth: 2, + pointRadius: 0, + pointHoverRadius: 3, + spanGaps: true + } + ] + }, + options: getTemperatureChartOptions(tickColor, gridColor, '°C') + }); + } else if (nozzleLeftCanvas) { + showNoDataMessage('nozzleTempLeftChart'); + } + // Initialize Bed Temperature Chart const bedCtx = document.getElementById('bedTempChart').getContext('2d'); bedTempChart = new Chart(bedCtx, { @@ -702,7 +746,7 @@ function updateChartTheme() { // Update all charts const charts = [ - nozzleTempChart, bedTempChart, printProgressChart, fanSpeedsChart, + nozzleTempChart, nozzleTempLeftChart, bedTempChart, printProgressChart, fanSpeedsChart, wifiSignalChart, amsConditionsChart, layerProgressChart, filamentTimelineChart ]; @@ -804,7 +848,7 @@ function applyDateSeparatorsToAllPrinterCharts(timestamps, dates) { const sepAnnotations = buildDateSeparatorAnnotations(timestamps, dates); const charts = [ - nozzleTempChart, bedTempChart, printProgressChart, fanSpeedsChart, + nozzleTempChart, nozzleTempLeftChart, bedTempChart, printProgressChart, fanSpeedsChart, wifiSignalChart, amsConditionsChart, layerProgressChart, filamentTimelineChart ]; diff --git a/bambu_run/static/bambu_run/js/printer_charts_control.js b/bambu_run/static/bambu_run/js/printer_charts_control.js index 3739674..e48d063 100644 --- a/bambu_run/static/bambu_run/js/printer_charts_control.js +++ b/bambu_run/static/bambu_run/js/printer_charts_control.js @@ -200,6 +200,13 @@ function updateAllPrinterCharts(data) { { data: data.nozzle_target_temp, datasetIndex: 1 } ]); + if (typeof nozzleTempLeftChart !== 'undefined' && nozzleTempLeftChart) { + updateChartData(nozzleTempLeftChart, data.timestamps, [ + { data: data.nozzle_temp_left || [], datasetIndex: 0 }, + { data: data.nozzle_target_temp_left || [], datasetIndex: 1 } + ]); + } + updateChartData(bedTempChart, data.timestamps, [ { data: data.bed_temp, datasetIndex: 0 }, { data: data.bed_target_temp, datasetIndex: 1 } @@ -269,7 +276,7 @@ function addProjectMarkersToCharts(markers, timestamps) { console.log('Adding project markers:', markers); const charts = [ - nozzleTempChart, bedTempChart, printProgressChart, fanSpeedsChart, + nozzleTempChart, nozzleTempLeftChart, bedTempChart, printProgressChart, fanSpeedsChart, wifiSignalChart, amsConditionsChart, layerProgressChart, filamentTimelineChart ]; @@ -400,7 +407,7 @@ function resetPrinterControls() { // Clear annotations and reload with original data const charts = [ - nozzleTempChart, bedTempChart, printProgressChart, fanSpeedsChart, + nozzleTempChart, nozzleTempLeftChart, bedTempChart, printProgressChart, fanSpeedsChart, wifiSignalChart, amsConditionsChart, layerProgressChart, filamentTimelineChart ]; diff --git a/bambu_run/templates/bambu_run/filament_list.html b/bambu_run/templates/bambu_run/filament_list.html index 8526386..e77cb7e 100644 --- a/bambu_run/templates/bambu_run/filament_list.html +++ b/bambu_run/templates/bambu_run/filament_list.html @@ -70,14 +70,22 @@ {% endfor %} -
+
-
+
+ +
+
Reset
@@ -149,7 +157,11 @@ {% if filament.is_loaded_in_ams %} - AMS Tray {{ filament.current_tray_id }} + + {% if filament.ams_type %}{{ filament.ams_type }}{% else %}AMS{% endif %} + {% if filament.ams_unit_id is not None %}#{{ filament.ams_unit_id }}{% endif %} + · Tray {{ filament.current_tray_id }} + {% else %} Storage {% endif %} diff --git a/bambu_run/templates/bambu_run/printer_dashboard.html b/bambu_run/templates/bambu_run/printer_dashboard.html index 58eeb95..3b0ecc7 100644 --- a/bambu_run/templates/bambu_run/printer_dashboard.html +++ b/bambu_run/templates/bambu_run/printer_dashboard.html @@ -22,7 +22,41 @@
- + {% if stats.is_dual_nozzle %} + +
+
+
+
+
+
Right Nozzle
+
{{ stats.nozzle_temp|floatformat:1 }}°C
+
target {{ stats.nozzle_target_temp|floatformat:0 }}°C + {% if stats.nozzle_type %}· {{ stats.nozzle_type }}{% endif %}
+
+ +
+
+
+
+ +
+
+
+
+
+
Left Nozzle
+
{{ stats.nozzle_temp_left|floatformat:1 }}°C
+
target {{ stats.nozzle_target_temp_left|floatformat:0 }}°C + {% if stats.nozzle_type_left %}· {{ stats.nozzle_type_left }}{% endif %}
+
+ +
+
+
+
+ {% else %} +
@@ -36,6 +70,7 @@
+ {% endif %}
@@ -266,10 +301,10 @@
- +
-
Nozzle Temperature
+
{% if stats.is_dual_nozzle %}Right Nozzle Temperature{% else %}Nozzle Temperature{% endif %}
@@ -278,6 +313,20 @@
+ {% if stats.is_dual_nozzle %} + +
+
+
Left Nozzle Temperature
+
+
+ +
+
+
+
+ {% endif %} +
diff --git a/bambu_run/views.py b/bambu_run/views.py index 1ca6a41..fb2991b 100644 --- a/bambu_run/views.py +++ b/bambu_run/views.py @@ -76,6 +76,14 @@ class PrinterDashboardView(LoginRequiredMixin, TemplateView): float(m.nozzle_target_temp) if m.nozzle_target_temp else None for m in metrics ], + "nozzle_temp_left": [ + float(m.nozzle_temp_left) if m.nozzle_temp_left is not None else None + for m in metrics + ], + "nozzle_target_temp_left": [ + float(m.nozzle_target_temp_left) if m.nozzle_target_temp_left is not None else None + for m in metrics + ], "bed_temp": [float(m.bed_temp) if m.bed_temp else None for m in metrics], "bed_target_temp": [ float(m.bed_target_temp) if m.bed_target_temp else None for m in metrics @@ -150,6 +158,14 @@ class PrinterDashboardView(LoginRequiredMixin, TemplateView): stats = { "nozzle_temp": float(latest_metric.nozzle_temp) if latest_metric.nozzle_temp else 0, + "nozzle_target_temp": float(latest_metric.nozzle_target_temp) if latest_metric.nozzle_target_temp else 0, + "nozzle_diameter": float(latest_metric.nozzle_diameter) if latest_metric.nozzle_diameter else None, + "nozzle_type": latest_metric.nozzle_type or "", + "nozzle_temp_left": float(latest_metric.nozzle_temp_left) if latest_metric.nozzle_temp_left is not None else None, + "nozzle_target_temp_left": float(latest_metric.nozzle_target_temp_left) if latest_metric.nozzle_target_temp_left is not None else None, + "nozzle_diameter_left": float(latest_metric.nozzle_diameter_left) if latest_metric.nozzle_diameter_left is not None else None, + "nozzle_type_left": latest_metric.nozzle_type_left or "", + "is_dual_nozzle": latest_metric.nozzle_temp_left is not None, "bed_temp": float(latest_metric.bed_temp) if latest_metric.bed_temp else 0, "chamber_temp": float(latest_metric.chamber_temp) if latest_metric.chamber_temp else 0, "print_percent": latest_metric.print_percent or 0, @@ -347,6 +363,8 @@ class PrinterDataAPIView(LoginRequiredMixin, View): dates = [] nozzle_temp = [] nozzle_target_temp = [] + nozzle_temp_left = [] + nozzle_target_temp_left = [] bed_temp = [] bed_target_temp = [] print_percent = [] @@ -374,6 +392,8 @@ class PrinterDataAPIView(LoginRequiredMixin, View): dates.append(ts.strftime('%Y-%m-%d')) nozzle_temp.append(float(m.nozzle_temp) if m.nozzle_temp else None) nozzle_target_temp.append(float(m.nozzle_target_temp) if m.nozzle_target_temp else None) + nozzle_temp_left.append(float(m.nozzle_temp_left) if m.nozzle_temp_left is not None else None) + nozzle_target_temp_left.append(float(m.nozzle_target_temp_left) if m.nozzle_target_temp_left is not None else None) bed_temp.append(float(m.bed_temp) if m.bed_temp else None) bed_target_temp.append(float(m.bed_target_temp) if m.bed_target_temp else None) print_percent.append(m.print_percent if m.print_percent else 0) @@ -451,6 +471,8 @@ class PrinterDataAPIView(LoginRequiredMixin, View): "dates": dates, "nozzle_temp": nozzle_temp, "nozzle_target_temp": nozzle_target_temp, + "nozzle_temp_left": nozzle_temp_left, + "nozzle_target_temp_left": nozzle_target_temp_left, "bed_temp": bed_temp, "bed_target_temp": bed_target_temp, "print_percent": print_percent, @@ -561,6 +583,10 @@ class FilamentListView(LoginRequiredMixin, ListView): elif loaded == 'no': queryset = queryset.filter(is_loaded_in_ams=False) + ams_type = self.request.GET.get('ams_type') + if ams_type: + queryset = queryset.filter(ams_type=ams_type) + search = self.request.GET.get('search') if search: queryset = queryset.filter( @@ -580,6 +606,11 @@ class FilamentListView(LoginRequiredMixin, ListView): context['filament_types'] = sorted( set(Filament.objects.exclude(type__isnull=True).exclude(type='').values_list('type', flat=True)) ) + context['ams_type_choices'] = sorted( + set( + Filament.objects.exclude(ams_type='').values_list('ams_type', flat=True) + ) + ) return context