mirror of
https://github.com/RunLit/Bambu-Run.git
synced 2026-06-22 22:19:03 +01:00
PrinterDataAPIView downsample
This commit is contained in:
@@ -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):
|
||||||
|
|||||||
Reference in New Issue
Block a user