PrinterDataAPIView downsample

This commit is contained in:
RNL
2026-03-04 22:40:18 +11:00
parent 5984bd6fa0
commit f20fc2ed06

View File

@@ -1,4 +1,4 @@
from datetime import timedelta from datetime import timedelta, datetime
from django.views.generic import TemplateView, View, ListView, CreateView, UpdateView, DetailView, DeleteView from django.views.generic import TemplateView, View, ListView, CreateView, UpdateView, DetailView, DeleteView
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.utils import timezone from django.utils import timezone
@@ -13,6 +13,19 @@ from .conf import app_settings
from .models import Printer, PrinterMetrics, Filament, FilamentColor, FilamentType, FilamentSnapshot, PrintJob, FilamentUsage from .models import Printer, PrinterMetrics, Filament, FilamentColor, FilamentType, FilamentSnapshot, PrintJob, FilamentUsage
from .forms import FilamentForm, FilamentColorForm, FilamentTypeForm from .forms import FilamentForm, FilamentColorForm, FilamentTypeForm
_METRICS_API_FIELDS = [
'id', 'device_id', 'timestamp',
'nozzle_temp', 'nozzle_target_temp',
'bed_temp', 'bed_target_temp',
'print_percent', 'cooling_fan_speed', 'heatbreak_fan_speed',
'wifi_signal_dbm', 'ams_humidity_raw', 'ams_temp',
'layer_num', 'total_layer_num',
'gcode_state', 'print_type', 'subtask_name',
'external_spool',
]
_MAX_CHART_POINTS = 3000
_FILAMENT_TIMELINE_MAX_STEP = 10 # disable filament timeline for ranges > ~10 days
class PrinterDashboardView(LoginRequiredMixin, TemplateView): class PrinterDashboardView(LoginRequiredMixin, TemplateView):
template_name = "bambu_run/printer_dashboard.html" template_name = "bambu_run/printer_dashboard.html"
@@ -248,110 +261,127 @@ class PrinterDataAPIView(LoginRequiredMixin, View):
if not printer_device: if not printer_device:
return JsonResponse({"error": "No printer device found"}, status=404) return JsonResponse({"error": "No printer device found"}, status=404)
query = PrinterMetrics.objects.filter(device=printer_device).prefetch_related('filament_snapshots')
tz = zoneinfo.ZoneInfo(app_settings.TIMEZONE) tz = zoneinfo.ZoneInfo(app_settings.TIMEZONE)
if start_date and start_time: # Stage A: only() + step calculation
from datetime import datetime query = (
start_dt_naive = datetime.strptime(f"{start_date} {start_time}", "%Y-%m-%d %H:%M") PrinterMetrics.objects
start_dt = start_dt_naive.replace(tzinfo=tz) .filter(device=printer_device)
.only(*_METRICS_API_FIELDS)
)
if start_date and start_time and end_date and end_time:
start_dt = datetime.strptime(f"{start_date} {start_time}", "%Y-%m-%d %H:%M").replace(tzinfo=tz)
end_dt = datetime.strptime(f"{end_date} {end_time}", "%Y-%m-%d %H:%M").replace(tzinfo=tz)
query = query.filter(timestamp__gte=start_dt, timestamp__lte=end_dt)
range_seconds = (end_dt - start_dt).total_seconds()
expected_count = max(1, int(range_seconds / 30))
elif start_date and start_time:
start_dt = datetime.strptime(f"{start_date} {start_time}", "%Y-%m-%d %H:%M").replace(tzinfo=tz)
query = query.filter(timestamp__gte=start_dt) query = query.filter(timestamp__gte=start_dt)
expected_count = _MAX_CHART_POINTS
if end_date and end_time: elif end_date and end_time:
from datetime import datetime end_dt = datetime.strptime(f"{end_date} {end_time}", "%Y-%m-%d %H:%M").replace(tzinfo=tz)
end_dt_naive = datetime.strptime(f"{end_date} {end_time}", "%Y-%m-%d %H:%M")
end_dt = end_dt_naive.replace(tzinfo=tz)
query = query.filter(timestamp__lte=end_dt) query = query.filter(timestamp__lte=end_dt)
expected_count = _MAX_CHART_POINTS
else:
expected_count = _MAX_CHART_POINTS
metrics = query.order_by("timestamp") step = max(1, expected_count // _MAX_CHART_POINTS)
data = { # Stage B: single DB round-trip, downsample in Python
"timestamps": [m.timestamp.astimezone(tz).strftime('%H:%M') for m in metrics], metrics_list = list(query.order_by("timestamp"))
"timestamps_iso": [m.timestamp.astimezone(tz).isoformat() for m in metrics], if step > 1:
"dates": [m.timestamp.astimezone(tz).strftime('%Y-%m-%d') for m in metrics], metrics_list = metrics_list[::step]
"nozzle_temp": [float(m.nozzle_temp) if m.nozzle_temp else None for m in metrics],
"nozzle_target_temp": [float(m.nozzle_target_temp) if m.nozzle_target_temp 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],
"print_percent": [m.print_percent if m.print_percent else 0 for m in metrics],
"cooling_fan_speed": [m.cooling_fan_speed if m.cooling_fan_speed else 0 for m in metrics],
"heatbreak_fan_speed": [m.heatbreak_fan_speed if m.heatbreak_fan_speed else 0 for m in metrics],
"wifi_signal_dbm": [m.wifi_signal_dbm if m.wifi_signal_dbm else None for m in metrics],
"ams_humidity_raw": [m.ams_humidity_raw if m.ams_humidity_raw else None for m in metrics],
"ams_temp": [float(m.ams_temp) if m.ams_temp else None for m in metrics],
"layer_num": [m.layer_num if m.layer_num else 0 for m in metrics],
"total_layer_num": [m.total_layer_num if m.total_layer_num else 0 for m in metrics],
"gcode_state": [m.gcode_state for m in metrics],
"print_type": [m.print_type for m in metrics],
"subtask_name": [m.subtask_name for m in metrics],
}
project_markers = self._calculate_project_markers(metrics, tz) total_points = len(metrics_list)
data["project_markers"] = project_markers
filament_timeline = self._prepare_filament_timeline_for_api(metrics) # Stage C: targeted snapshot fetch (only sampled IDs)
data["filament_timeline"] = filament_timeline include_filament = (step <= _FILAMENT_TIMELINE_MAX_STEP)
snapshots_by_metric: dict = {}
if include_filament and metrics_list:
sampled_ids = [m.id for m in metrics_list]
for snap in FilamentSnapshot.objects.filter(printer_metric_id__in=sampled_ids):
snapshots_by_metric.setdefault(snap.printer_metric_id, []).append(snap)
return JsonResponse(data) # Stage D: single-pass serialization
timestamps = []
timestamps_iso = []
dates = []
nozzle_temp = []
nozzle_target_temp = []
bed_temp = []
bed_target_temp = []
print_percent = []
cooling_fan_speed = []
heatbreak_fan_speed = []
wifi_signal_dbm = []
ams_humidity_raw = []
ams_temp = []
layer_num = []
total_layer_num = []
gcode_state = []
print_type = []
subtask_name = []
except Exception as e: project_markers = []
import traceback
traceback.print_exc()
return JsonResponse({"error": str(e)}, status=500)
def _calculate_project_markers(self, metrics, timezone_info):
markers = []
current_job = None current_job = None
last_state = None last_state = None
for idx, metric in enumerate(metrics): filament_data = {}
subtask = metric.subtask_name
gcode_state = metric.gcode_state
is_printing = gcode_state not in ['FINISH', 'IDLE', None, ''] for idx, m in enumerate(metrics_list):
ts = m.timestamp.astimezone(tz)
timestamps.append(ts.strftime('%H:%M'))
timestamps_iso.append(ts.isoformat())
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)
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)
cooling_fan_speed.append(m.cooling_fan_speed if m.cooling_fan_speed else 0)
heatbreak_fan_speed.append(m.heatbreak_fan_speed if m.heatbreak_fan_speed else 0)
wifi_signal_dbm.append(m.wifi_signal_dbm if m.wifi_signal_dbm else None)
ams_humidity_raw.append(m.ams_humidity_raw if m.ams_humidity_raw else None)
ams_temp.append(float(m.ams_temp) if m.ams_temp else None)
layer_num.append(m.layer_num if m.layer_num else 0)
total_layer_num.append(m.total_layer_num if m.total_layer_num else 0)
gcode_state.append(m.gcode_state)
print_type.append(m.print_type)
subtask_name.append(m.subtask_name)
# Project marker detection (inline)
subtask = m.subtask_name
gs = m.gcode_state
is_printing = gs not in ['FINISH', 'IDLE', None, '']
if subtask and subtask != current_job and is_printing: if subtask and subtask != current_job and is_printing:
markers.append({ project_markers.append({
'type': 'start', 'type': 'start',
'index': idx, 'index': idx,
'timestamp': metric.timestamp.astimezone(timezone_info).isoformat(), 'timestamp': ts.isoformat(),
'project_name': subtask, 'project_name': subtask,
}) })
current_job = subtask current_job = subtask
last_state = gcode_state last_state = gs
elif current_job and last_state and last_state not in ['FINISH', 'IDLE'] and gs in ['FINISH', 'IDLE']:
elif current_job and last_state and last_state not in ['FINISH', 'IDLE'] and gcode_state in ['FINISH', 'IDLE']: project_markers.append({
markers.append({
'type': 'end', 'type': 'end',
'index': idx, 'index': idx,
'timestamp': metric.timestamp.astimezone(timezone_info).isoformat(), 'timestamp': ts.isoformat(),
'project_name': current_job, 'project_name': current_job,
}) })
current_job = None current_job = None
last_state = gs
last_state = gcode_state # Filament timeline (inline, only when include_filament)
if include_filament:
return markers for snap in snapshots_by_metric.get(m.id, []):
tray_id = snap.tray_id
def _prepare_filament_timeline_for_api(self, metrics): fil_type = snap.type or 'Unknown'
filament_data = {} fil_sub_type = snap.sub_type or 'Unknown'
total_points = len(metrics) fil_color = snap.color or 'FFFFFFFF'
for idx, metric in enumerate(metrics):
try:
snapshots = metric.filament_snapshots.all()
except Exception:
snapshots = []
for snapshot in snapshots:
tray_id = snapshot.tray_id
fil_type = snapshot.type or 'Unknown'
fil_sub_type = snapshot.sub_type or 'Unknown'
fil_color = snapshot.color or 'FFFFFFFF'
unique_key = f"{tray_id}_{fil_type}_{fil_sub_type}_{fil_color}" unique_key = f"{tray_id}_{fil_type}_{fil_sub_type}_{fil_color}"
if unique_key not in filament_data: if unique_key not in filament_data:
filament_data[unique_key] = { filament_data[unique_key] = {
'tray_id': tray_id, 'tray_id': tray_id,
@@ -361,17 +391,13 @@ class PrinterDataAPIView(LoginRequiredMixin, View):
'remain_data': [None] * total_points, 'remain_data': [None] * total_points,
'start_idx': idx, 'start_idx': idx,
} }
filament_data[unique_key]['remain_data'][idx] = snap.remain_percent or 0
remain_percent = snapshot.remain_percent or 0 external = m.external_spool or {}
filament_data[unique_key]['remain_data'][idx] = remain_percent
for idx, metric in enumerate(metrics):
external = metric.external_spool or {}
if external.get('type'): if external.get('type'):
fil_type = external.get('type', 'Unknown') fil_type = external.get('type', 'Unknown')
fil_color = external.get('color', '161616FF') fil_color = external.get('color', '161616FF')
unique_key = f"External_{fil_type}_{fil_color}" unique_key = f"External_{fil_type}_{fil_color}"
if unique_key not in filament_data: if unique_key not in filament_data:
filament_data[unique_key] = { filament_data[unique_key] = {
'tray_id': 'External', 'tray_id': 'External',
@@ -381,11 +407,37 @@ class PrinterDataAPIView(LoginRequiredMixin, View):
'remain_data': [None] * total_points, 'remain_data': [None] * total_points,
'start_idx': idx, 'start_idx': idx,
} }
filament_data[unique_key]['remain_data'][idx] = external.get('remain', 0)
remain_percent = external.get('remain', 0) data = {
filament_data[unique_key]['remain_data'][idx] = remain_percent "timestamps": timestamps,
"timestamps_iso": timestamps_iso,
"dates": dates,
"nozzle_temp": nozzle_temp,
"nozzle_target_temp": nozzle_target_temp,
"bed_temp": bed_temp,
"bed_target_temp": bed_target_temp,
"print_percent": print_percent,
"cooling_fan_speed": cooling_fan_speed,
"heatbreak_fan_speed": heatbreak_fan_speed,
"wifi_signal_dbm": wifi_signal_dbm,
"ams_humidity_raw": ams_humidity_raw,
"ams_temp": ams_temp,
"layer_num": layer_num,
"total_layer_num": total_layer_num,
"gcode_state": gcode_state,
"print_type": print_type,
"subtask_name": subtask_name,
"project_markers": project_markers,
"filament_timeline": filament_data,
}
return filament_data return JsonResponse(data)
except Exception as e:
import traceback
traceback.print_exc()
return JsonResponse({"error": str(e)}, status=500)
class FilamentUsageDataAPIView(LoginRequiredMixin, View): class FilamentUsageDataAPIView(LoginRequiredMixin, View):