5 Commits

Author SHA1 Message Date
RNL
ffaee007a2 use correct bump-version 2026-03-29 23:13:31 +11:00
RNL
34ef5d6973 back fill and relink print name using cloud if there is 2026-03-29 23:08:31 +11:00
RNL
9978b7027a added bambu cloud task sync with correct endpoint other than py cloud api 2026-03-29 22:48:02 +11:00
RNL
e551dcc5fd timestamp use your local django timezone 2026-03-29 15:44:27 +11:00
RNL
d167073fde added mcp initial trail files 2026-03-28 22:53:59 +11:00
12 changed files with 27 additions and 487 deletions

View File

@@ -1,56 +0,0 @@
name: Bump Patch Version on Merge to Main
on:
push:
branches:
- main
jobs:
bump-version:
runs-on: ubuntu-latest
# Skip if this push was itself the version bump commit (prevents infinite loop)
if: "!contains(github.event.head_commit.message, '[skip ci]')"
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
# Need full git history and ability to push
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Bump patch version in pyproject.toml
id: bump
run: |
# Read current version
CURRENT=$(grep '^version = ' pyproject.toml | sed 's/version = "\(.*\)"/\1/')
echo "Current version: $CURRENT"
# Split into parts and increment patch
MAJOR=$(echo $CURRENT | cut -d. -f1)
MINOR=$(echo $CURRENT | cut -d. -f2)
PATCH=$(echo $CURRENT | cut -d. -f3)
NEW_PATCH=$((PATCH + 1))
NEW_VERSION="$MAJOR.$MINOR.$NEW_PATCH"
echo "New version: $NEW_VERSION"
# Write back to pyproject.toml
sed -i "s/^version = \"$CURRENT\"/version = \"$NEW_VERSION\"/" pyproject.toml
# Export for later steps
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
- name: Commit and push bumped version
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add pyproject.toml
git commit -m "chore: bump version to ${{ steps.bump.outputs.new_version }} [skip ci]"
git push
- name: Tag the release
run: |
git tag "v${{ steps.bump.outputs.new_version }}"
git push origin "v${{ steps.bump.outputs.new_version }}"

View File

@@ -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', 'ams_unit_id', 'ams_type',
'is_loaded_in_ams', 'current_tray_id',
'purchase_date', 'purchase_price', 'supplier', 'notes'
]
widgets = {
@@ -87,15 +87,7 @@ 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': '15',
'placeholder': '03 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'}),
'current_tray_id': forms.NumberInput(attrs={'class': 'form-control', 'min': '0', 'max': '3'}),
'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'}),
@@ -114,8 +106,6 @@ 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()

View File

@@ -111,8 +111,6 @@ 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:
@@ -124,24 +122,6 @@ 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)
@@ -187,11 +167,6 @@ 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):
@@ -402,8 +377,6 @@ 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(),
)
@@ -417,13 +390,9 @@ class Command(BaseCommand):
return filament
def _update_filament_status(self, filament, tray_id, remain_percent, tray_data=None):
def _update_filament_status(self, filament, tray_id, remain_percent):
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()
@@ -431,19 +400,10 @@ class Command(BaseCommand):
if self.verbose:
logger.debug(f"Updated filament {filament}: {remain_percent}%")
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(
if not filament.is_loaded_in_ams or filament.current_tray_id != tray_id:
previous_filament = Filament.objects.filter(
is_loaded_in_ams=True, current_tray_id=tray_id
).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()
).exclude(id=filament.id).first()
if previous_filament:
previous_filament.is_loaded_in_ams = False
@@ -451,21 +411,14 @@ class Command(BaseCommand):
previous_filament.save()
logger.info(
f"Auto-unloaded {previous_filament} from Tray {tray_id} "
f"(unit {ams_unit_id}; replaced by {filament.brand} {filament.type} - {filament.color})"
f"(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: 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
logger.debug(f"Updated filament location: Tray {tray_id}")
filament.save()
@@ -486,13 +439,10 @@ 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, tray_data)
self._update_filament_status(filament, tray_id, remain_percent)
# 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 {}
unit_id = str(int(tray_id) // 4) if tray_id.isdigit() else None
unit_data = ams_units.get(unit_id, {})
FilamentSnapshot.objects.create(
printer_metric=printer_metric,
@@ -645,10 +595,6 @@ 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"),

View File

@@ -1,90 +0,0 @@
# 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,
),
),
]

View File

@@ -2,33 +2,6 @@ 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"""
@@ -85,32 +58,12 @@ class PrinterMetrics(models.Model):
max_digits=5, decimal_places=2, null=True, blank=True
)
# 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 info
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."
@@ -412,16 +365,7 @@ class Filament(models.Model):
)
current_tray_id = models.IntegerField(
null=True, blank=True,
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)"
help_text="Which AMS slot (0-3) if loaded"
)
last_loaded_date = models.DateTimeField(
null=True, blank=True,

View File

@@ -335,16 +335,10 @@ class PrinterState:
wifi_signal: str = ""
wifi_signal_dbm: int = 0
# Nozzle info — single-nozzle / right-side back-compat fields.
# Nozzle info
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
@@ -416,21 +410,6 @@ 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", "")),
@@ -459,13 +438,6 @@ 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", "")),
@@ -501,14 +473,6 @@ 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,
@@ -551,19 +515,8 @@ 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({
@@ -589,9 +542,6 @@ 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
@@ -602,7 +552,6 @@ 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,

View File

@@ -1,7 +1,7 @@
// 3D Printer Charts Initialization and Management
// Chart.js implementation for printer metrics visualization
let nozzleTempChart, nozzleTempLeftChart, bedTempChart, printProgressChart, fanSpeedsChart;
let nozzleTempChart, bedTempChart, printProgressChart, fanSpeedsChart;
let wifiSignalChart, amsConditionsChart, layerProgressChart, filamentTimelineChart;
function showNoDataMessage(canvasId) {
@@ -75,50 +75,6 @@ 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, {
@@ -746,7 +702,7 @@ function updateChartTheme() {
// Update all charts
const charts = [
nozzleTempChart, nozzleTempLeftChart, bedTempChart, printProgressChart, fanSpeedsChart,
nozzleTempChart, bedTempChart, printProgressChart, fanSpeedsChart,
wifiSignalChart, amsConditionsChart, layerProgressChart, filamentTimelineChart
];
@@ -848,7 +804,7 @@ function applyDateSeparatorsToAllPrinterCharts(timestamps, dates) {
const sepAnnotations = buildDateSeparatorAnnotations(timestamps, dates);
const charts = [
nozzleTempChart, nozzleTempLeftChart, bedTempChart, printProgressChart, fanSpeedsChart,
nozzleTempChart, bedTempChart, printProgressChart, fanSpeedsChart,
wifiSignalChart, amsConditionsChart, layerProgressChart, filamentTimelineChart
];

View File

@@ -200,13 +200,6 @@ 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 }
@@ -276,7 +269,7 @@ function addProjectMarkersToCharts(markers, timestamps) {
console.log('Adding project markers:', markers);
const charts = [
nozzleTempChart, nozzleTempLeftChart, bedTempChart, printProgressChart, fanSpeedsChart,
nozzleTempChart, bedTempChart, printProgressChart, fanSpeedsChart,
wifiSignalChart, amsConditionsChart, layerProgressChart, filamentTimelineChart
];
@@ -407,7 +400,7 @@ function resetPrinterControls() {
// Clear annotations and reload with original data
const charts = [
nozzleTempChart, nozzleTempLeftChart, bedTempChart, printProgressChart, fanSpeedsChart,
nozzleTempChart, bedTempChart, printProgressChart, fanSpeedsChart,
wifiSignalChart, amsConditionsChart, layerProgressChart, filamentTimelineChart
];

View File

@@ -70,22 +70,14 @@
{% endfor %}
</select>
</div>
<div class="col-md-2">
<div class="col-md-3">
<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-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">
<div class="col-md-3">
<button type="submit" class="btn btn-secondary">Filter</button>
<a href="{% url 'bambu_run:filament_list' %}" class="btn btn-outline-secondary">Reset</a>
</div>
@@ -157,11 +149,7 @@
</td>
<td class="align-middle">
{% if filament.is_loaded_in_ams %}
<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>
<span class="badge bg-success">AMS Tray {{ filament.current_tray_id }}</span>
{% else %}
<span class="badge bg-secondary">Storage</span>
{% endif %}

View File

@@ -22,41 +22,7 @@
<!-- Summary Cards Row -->
<div class="row g-3 mb-4">
{% 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) -->
<!-- Nozzle Temperature Card -->
<div class="col-12 col-md-6 col-lg-3">
<div class="card infra-card-warning">
<div class="card-body">
@@ -70,7 +36,6 @@
</div>
</div>
</div>
{% endif %}
<!-- Bed Temperature Card -->
<div class="col-12 col-md-6 col-lg-3">
@@ -301,10 +266,10 @@
<!-- Charts Section -->
<div class="row g-3 mb-4">
<!-- Nozzle Temperature Chart (right side / single nozzle) -->
<!-- Nozzle Temperature Chart -->
<div class="col-12 col-lg-6">
<div class="card">
<div class="card-header">{% if stats.is_dual_nozzle %}Right Nozzle Temperature{% else %}Nozzle Temperature{% endif %}</div>
<div class="card-header">Nozzle Temperature</div>
<div class="card-body">
<div class="chart-container">
<canvas id="nozzleTempChart"></canvas>
@@ -313,20 +278,6 @@
</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">

View File

@@ -76,14 +76,6 @@ 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
@@ -158,14 +150,6 @@ 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,
@@ -363,8 +347,6 @@ class PrinterDataAPIView(LoginRequiredMixin, View):
dates = []
nozzle_temp = []
nozzle_target_temp = []
nozzle_temp_left = []
nozzle_target_temp_left = []
bed_temp = []
bed_target_temp = []
print_percent = []
@@ -392,8 +374,6 @@ 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)
@@ -471,8 +451,6 @@ 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,
@@ -583,10 +561,6 @@ 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(
@@ -606,11 +580,6 @@ 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

View File

@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
[project]
name = "bambu-run"
version = "0.1.6"
version = "0.1.3"
description = "Django reusable app for Bambu Lab 3D printer monitoring and filament inventory management"
readme = "README.md"
license = {text = "MIT"}
requires-python = ">=3.9"
requires-python = ">=3.10"
authors = [
{name = "Runnan Li"},
]