Add H2C dual-nozzle and multi-AMS-type support

Schema (migration 0004):
- PrinterMetrics: nozzle_temp_left, nozzle_target_temp_left,
  nozzle_diameter_left, nozzle_type_left (all nullable)
- Filament: ams_unit_id (nullable int), ams_type (AMS/AMS 2 Pro/AMS HT)
- AMS_INFO_TO_TYPE map and AMS_TYPE_CHOICES on models

Parser (mqtt_client.py):
- Decode bit-packed temps from device.extruder.info[] for left/right nozzle
- Emit per-nozzle fields in get_snapshot(); legacy keys mirror right side
- AMS unit type from info code per unit dict

Collector (bambu_collector.py):
- Write left-nozzle fields to PrinterMetrics
- Set ams_unit_id + ams_type on Filament records
- Fix: poll MQTTClient.connected before pushall (not BambuPrinter._connected)
- Add 5s post-pushall wait in --once mode so response arrives before collect

Views: API and dashboard include left-nozzle series; is_dual_nozzle flag
Templates: dual-nozzle cards + chart; AMS-type badge + filter on filament list
Charts: left nozzle temp chart with conditional render
Forms: fix tray_id max=3 → max=15; add ams_unit_id, ams_type fields
This commit is contained in:
RNL
2026-05-07 14:51:31 +10:00
parent 6fadccb527
commit dd57a963ac
10 changed files with 429 additions and 25 deletions

View File

@@ -70,14 +70,22 @@
{% endfor %}
</select>
</div>
<div class="col-md-3">
<div class="col-md-2">
<select name="loaded" class="form-select">
<option value="">All Spools</option>
<option value="yes" {% if request.GET.loaded == 'yes' %}selected{% endif %}>Loaded in AMS</option>
<option value="no" {% if request.GET.loaded == 'no' %}selected{% endif %}>Not Loaded</option>
</select>
</div>
<div class="col-md-3">
<div class="col-md-2">
<select name="ams_type" class="form-select">
<option value="">All AMS Types</option>
{% for at in ams_type_choices %}
<option value="{{ at }}" {% if request.GET.ams_type == at %}selected{% endif %}>{{ at }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-secondary">Filter</button>
<a href="{% url 'bambu_run:filament_list' %}" class="btn btn-outline-secondary">Reset</a>
</div>
@@ -149,7 +157,11 @@
</td>
<td class="align-middle">
{% if filament.is_loaded_in_ams %}
<span class="badge bg-success">AMS Tray {{ filament.current_tray_id }}</span>
<span class="badge bg-success">
{% 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 }}
</span>
{% else %}
<span class="badge bg-secondary">Storage</span>
{% endif %}

View File

@@ -22,7 +22,41 @@
<!-- Summary Cards Row -->
<div class="row g-3 mb-4">
<!-- Nozzle Temperature Card -->
{% if stats.is_dual_nozzle %}
<!-- Right Nozzle (dual-nozzle printers, e.g. H2C) -->
<div class="col-12 col-md-6 col-lg-3">
<div class="card infra-card-warning">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start">
<div>
<div class="stat-label">Right Nozzle</div>
<div class="stat-value">{{ stats.nozzle_temp|floatformat:1 }}&deg;C</div>
<div class="text-muted small">target {{ stats.nozzle_target_temp|floatformat:0 }}&deg;C
{% if stats.nozzle_type %}· {{ stats.nozzle_type }}{% endif %}</div>
</div>
<i class="bi bi-thermometer-high" style="font-size: 2rem; opacity: 0.3;"></i>
</div>
</div>
</div>
</div>
<!-- Left Nozzle -->
<div class="col-12 col-md-6 col-lg-3">
<div class="card infra-card-warning">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start">
<div>
<div class="stat-label">Left Nozzle</div>
<div class="stat-value">{{ stats.nozzle_temp_left|floatformat:1 }}&deg;C</div>
<div class="text-muted small">target {{ stats.nozzle_target_temp_left|floatformat:0 }}&deg;C
{% if stats.nozzle_type_left %}· {{ stats.nozzle_type_left }}{% endif %}</div>
</div>
<i class="bi bi-thermometer-high" style="font-size: 2rem; opacity: 0.3;"></i>
</div>
</div>
</div>
</div>
{% else %}
<!-- Nozzle Temperature Card (single-nozzle printers) -->
<div class="col-12 col-md-6 col-lg-3">
<div class="card infra-card-warning">
<div class="card-body">
@@ -36,6 +70,7 @@
</div>
</div>
</div>
{% endif %}
<!-- Bed Temperature Card -->
<div class="col-12 col-md-6 col-lg-3">
@@ -266,10 +301,10 @@
<!-- Charts Section -->
<div class="row g-3 mb-4">
<!-- Nozzle Temperature Chart -->
<!-- Nozzle Temperature Chart (right side / single nozzle) -->
<div class="col-12 col-lg-6">
<div class="card">
<div class="card-header">Nozzle Temperature</div>
<div class="card-header">{% if stats.is_dual_nozzle %}Right Nozzle Temperature{% else %}Nozzle Temperature{% endif %}</div>
<div class="card-body">
<div class="chart-container">
<canvas id="nozzleTempChart"></canvas>
@@ -278,6 +313,20 @@
</div>
</div>
{% if stats.is_dual_nozzle %}
<!-- Left Nozzle Temperature Chart (H2C-class dual-nozzle) -->
<div class="col-12 col-lg-6">
<div class="card">
<div class="card-header">Left Nozzle Temperature</div>
<div class="card-body">
<div class="chart-container">
<canvas id="nozzleTempLeftChart"></canvas>
</div>
</div>
</div>
</div>
{% endif %}
<!-- Bed Temperature Chart -->
<div class="col-12 col-lg-6">
<div class="card">