diff --git a/README.md b/README.md index 0cf1ddd3..f7142860 100644 --- a/README.md +++ b/README.md @@ -177,6 +177,7 @@ SpoolMan can print QR-code stickers for every spool; follow the SpoolMan label g - set `TRACK_LAYER_USAGE` to `True` to switch to per-layer tracking/consumption **while `AUTO_SPEND` is also `True`**. If `AUTO_SPEND` is `False`, all filament tracking remains disabled regardless of `TRACK_LAYER_USAGE`. - set `AUTO_SPEND` to `True` if you want automatic filament usage tracking (see the AUTO SPEND notes below). - set `DISABLE_MISMATCH_WARNING` to `True` to hide mismatch warnings in the UI (mismatches are still detected and logged to `data/filament_mismatch.json`). + - set `CLEAR_ASSIGNMENT_WHEN_EMPTY` to `True` if you want OpenSpoolMan to clear any SpoolMan assignment and reset the AMS tray whenever the printer reports no spool in that slot. - By default, the app reads `data/3d_printer_logs.db` for print history; override it through `OPENSPOOLMAN_PRINT_HISTORY_DB` or via the screenshot helper (which targets `data/demo.db` by default). - Run SpoolMan. diff --git a/app.py b/app.py index 8aee01ab..ed760f1c 100644 --- a/app.py +++ b/app.py @@ -3,6 +3,7 @@ import os import traceback import uuid +from collections import Counter from flask import Flask, request, render_template, redirect, url_for @@ -38,7 +39,51 @@ @app.context_processor def fronted_utilities(): - return dict(SPOOLMAN_BASE_URL=SPOOLMAN_BASE_URL, AUTO_SPEND=AUTO_SPEND, color_is_dark=color_is_dark, BASE_URL=BASE_URL, EXTERNAL_SPOOL_AMS_ID=EXTERNAL_SPOOL_AMS_ID, EXTERNAL_SPOOL_ID=EXTERNAL_SPOOL_ID, PRINTER_MODEL=mqtt_bambulab.getPrinterModel(), PRINTER_NAME=PRINTER_NAME) + printer_model = mqtt_bambulab.getPrinterModel() or {} + ams_models_by_id = mqtt_bambulab.getDetectedAmsModelsById() + + return dict( + SPOOLMAN_BASE_URL=SPOOLMAN_BASE_URL, + AUTO_SPEND=AUTO_SPEND, + AMS_MODELS_BY_ID=ams_models_by_id, + color_is_dark=color_is_dark, + BASE_URL=BASE_URL, + EXTERNAL_SPOOL_AMS_ID=EXTERNAL_SPOOL_AMS_ID, + EXTERNAL_SPOOL_ID=EXTERNAL_SPOOL_ID, + PRINTER_MODEL=printer_model, + PRINTER_NAME=PRINTER_NAME, + ) + + +def build_ams_labels(ams_data): + models_by_id = mqtt_bambulab.getDetectedAmsModelsById() + base_labels = [] + for ams in ams_data: + ams_id = ams.get("id") + key = str(ams_id) + base_name = models_by_id.get(key) or models_by_id.get(ams_id) or "AMS" + base_labels.append(base_name) + + totals = Counter(base_labels) + takt = Counter() + labels = {} + + for ams, base_name in zip(ams_data, base_labels): + takt[base_name] += 1 + suffix = f" {takt[base_name]}" if totals[base_name] > 1 else "" + label = f"{base_name}{suffix}" + ams_id = ams.get("id") + labels[str(ams_id)] = label + labels[ams_id] = label + + return labels + + +def _augment_tray(spool_list, tray_data, ams_id, tray_id): + augmentTrayDataWithSpoolMan(spool_list, tray_data, ams_id, tray_id) + if tray_data.get("unmapped_bambu_tag"): + spoolman_service.clear_active_spool_for_tray(ams_id, tray_id) + augmentTrayDataWithSpoolMan(spool_list, tray_data, ams_id, tray_id) @app.route("/issue") def issue(): @@ -72,12 +117,12 @@ def issue(): active_spool = None for spool in spool_list: - if spool.get("extra") and spool["extra"].get("active_tray") and spool["extra"]["active_tray"] == json.dumps(trayUid(ams_id, tray_id)): + if spool.get("extra") and spool["extra"].get("active_tray") and spool["extra"].get("active_tray") == json.dumps(trayUid(ams_id, tray_id)): active_spool = spool break if tray_data: - augmentTrayDataWithSpoolMan(spool_list, tray_data, trayUid(ams_id, tray_id)) + _augment_tray(spool_list, tray_data, ams_id, tray_id) #TODO: Determine issue #New bambulab spool @@ -135,6 +180,73 @@ def fill(): return render_template('fill.html', spools=spools, ams_id=ams_id, tray_id=tray_id, materials=materials, selected_materials=selected_materials) +@app.route("/assign_bambu_spool") +def assign_bambu_spool(): + if not mqtt_bambulab.isMqttClientConnected(): + return render_template('error.html', exception="MQTT is disconnected. Is the printer online?") + + bambu_tag = request.args.get("tag") + ams_id = request.args.get("ams") + tray_id = request.args.get("tray") + spool_id = request.args.get("spool_id") + + if not all([bambu_tag, ams_id, tray_id]): + return render_template('error.html', exception="Missing AMS ID, Tray ID, or Bambu spool tag.") + + if bambu_tag == "00000000000000000000000000000000": + return render_template('error.html', exception="No Bambu spool was detected in this tray.") + + if spool_id: + if READ_ONLY_MODE: + return render_template('error.html', exception="Live read-only mode: linking Bambu spools is disabled.") + + spool_data = spoolman_client.getSpoolById(spool_id) + extras = spool_data.get("extra") or {} + + spoolman_client.patchExtraTags(spool_id, extras, { + "tag": json.dumps(bambu_tag), + }) + + mqtt_bambulab.setActiveTray(spool_id, extras, ams_id, tray_id) + setActiveSpool(ams_id, tray_id, spool_data) + + return redirect(url_for('home', success_message=f"Linked Bambu spool to SpoolMan spool {spool_id} on AMS {ams_id}, Tray {tray_id}.")) + + spools = mqtt_bambulab.fetchSpools() + materials = extract_materials(spools) + selected_materials = [] + + try: + last_ams_config = mqtt_bambulab.getLastAMSConfig() + default_material = None + + if ams_id == EXTERNAL_SPOOL_AMS_ID: + default_material = last_ams_config.get("vt_tray", {}).get("tray_type") + else: + for ams in last_ams_config.get("ams", []): + if str(ams.get("id")) != str(ams_id): + continue + + for tray in ams.get("tray", []): + if str(tray.get("id")) == str(tray_id): + default_material = tray.get("tray_type") + break + + if default_material and default_material in materials: + selected_materials.append(default_material) + except Exception: + pass + + return render_template( + 'assign_bambu_spool.html', + spools=spools, + ams_id=ams_id, + tray_id=tray_id, + bambu_tag=bambu_tag, + materials=materials, + selected_materials=selected_materials, + ) + @app.route("/spool_info") def spool_info(): if not mqtt_bambulab.isMqttClientConnected(): @@ -150,12 +262,12 @@ def spool_info(): issue = False #TODO: Fix issue when external spool info is reset via bambulab interface - augmentTrayDataWithSpoolMan(spool_list, vt_tray_data, trayUid(EXTERNAL_SPOOL_AMS_ID, EXTERNAL_SPOOL_ID)) + _augment_tray(spool_list, vt_tray_data, EXTERNAL_SPOOL_AMS_ID, EXTERNAL_SPOOL_ID) issue |= vt_tray_data.get("issue", False) for ams in ams_data: for tray in ams["tray"]: - augmentTrayDataWithSpoolMan(spool_list, tray, trayUid(ams["id"], tray["id"])) + _augment_tray(spool_list, tray, ams["id"], tray["id"]) issue |= tray.get("issue", False) if not tag_id and not spool_id: @@ -194,7 +306,8 @@ def spool_info(): break if current_spool: - return render_template('spool_info.html', tag_id=tag_id, current_spool=current_spool, ams_data=ams_data, vt_tray_data=vt_tray_data, issue=issue) + ams_labels = build_ams_labels(ams_data) + return render_template('spool_info.html', tag_id=tag_id, current_spool=current_spool, ams_data=ams_data, vt_tray_data=vt_tray_data, issue=issue, ams_labels=ams_labels) else: return render_template('error.html', exception="Spool not found") except Exception as e: @@ -302,15 +415,16 @@ def home(): issue = False #TODO: Fix issue when external spool info is reset via bambulab interface - augmentTrayDataWithSpoolMan(spool_list, vt_tray_data, trayUid(EXTERNAL_SPOOL_AMS_ID, EXTERNAL_SPOOL_ID)) + _augment_tray(spool_list, vt_tray_data, EXTERNAL_SPOOL_AMS_ID, EXTERNAL_SPOOL_ID) issue |= vt_tray_data["issue"] for ams in ams_data: for tray in ams["tray"]: - augmentTrayDataWithSpoolMan(spool_list, tray, trayUid(ams["id"], tray["id"])) + _augment_tray(spool_list, tray, ams["id"], tray["id"]) issue |= tray["issue"] - return render_template('index.html', success_message=success_message, ams_data=ams_data, vt_tray_data=vt_tray_data, issue=issue) + ams_labels = build_ams_labels(ams_data) + return render_template('index.html', success_message=success_message, ams_data=ams_data, vt_tray_data=vt_tray_data, issue=issue, ams_labels=ams_labels) except Exception as e: traceback.print_exc() return render_template('error.html', exception=str(e)) diff --git a/config.env.template b/config.env.template index 0c257391..b3552f9d 100644 --- a/config.env.template +++ b/config.env.template @@ -6,5 +6,6 @@ PRINTER_IP= PRINTER_NAME= SPOOLMAN_BASE_URL= AUTO_SPEND=False -TRACK_LAYER_USAGE=False -SPOOL_SORTING=filament.material:asc,filament.vendor.name:asc,filament.name:asc \ No newline at end of file +TRACK_LAYER_USAGE=False +SPOOL_SORTING=filament.material:asc,filament.vendor.name:asc,filament.name:asc +CLEAR_ASSIGNMENT_WHEN_EMPTY=False diff --git a/config.py b/config.py index 152b6aef..84bd3281 100644 --- a/config.py +++ b/config.py @@ -33,3 +33,4 @@ def _env_to_bool(name: str, default: bool = False) -> bool: "SPOOL_SORTING", "filament.material:asc,filament.vendor.name:asc,filament.name:asc" ) DISABLE_MISMATCH_WARNING = _env_to_bool("DISABLE_MISMATCH_WARNING", False) +CLEAR_ASSIGNMENT_WHEN_EMPTY = _env_to_bool("CLEAR_ASSIGNMENT_WHEN_EMPTY", False) diff --git a/docker-compose.yaml b/docker-compose.yaml index 144f079d..53f12052 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -4,7 +4,7 @@ services: build: . env_file: "config.env" ports: - - "8001:8001" + - "8001:8000" volumes: - ./logs:/home/app/logs - ./data:/home/app/data diff --git a/mqtt_bambulab.py b/mqtt_bambulab.py index 19546371..673a12af 100644 --- a/mqtt_bambulab.py +++ b/mqtt_bambulab.py @@ -1,13 +1,24 @@ + + import json import ssl import traceback from threading import Thread +from typing import Any, Iterable import paho.mqtt.client as mqtt -from config import PRINTER_ID, PRINTER_CODE, PRINTER_IP, AUTO_SPEND, EXTERNAL_SPOOL_ID, TRACK_LAYER_USAGE -from messages import GET_VERSION, PUSH_ALL -from spoolman_service import spendFilaments, setActiveTray, fetchSpools +from config import ( + PRINTER_ID, + PRINTER_CODE, + PRINTER_IP, + AUTO_SPEND, + EXTERNAL_SPOOL_ID, + TRACK_LAYER_USAGE, + CLEAR_ASSIGNMENT_WHEN_EMPTY, +) +from messages import GET_VERSION, PUSH_ALL, AMS_FILAMENT_SETTING +from spoolman_service import spendFilaments, setActiveTray, fetchSpools, clear_active_spool_for_tray from tools_3mf import getMetaDataFrom3mf import time import copy @@ -15,7 +26,6 @@ from logger import append_to_rotating_file from print_history import insert_print, insert_filament_usage from filament_usage_tracker import FilamentUsageTracker - MQTT_CLIENT = {} # Global variable storing MQTT Client MQTT_CLIENT_CONNECTED = False MQTT_KEEPALIVE = 60 @@ -26,6 +36,7 @@ PENDING_PRINT_METADATA = {} FILAMENT_TRACKER = FilamentUsageTracker() +LOG_FILE = "/home/app/logs/mqtt.log" def getPrinterModel(): global PRINTER_ID @@ -67,6 +78,74 @@ def getPrinterModel(): "devicename": device_name } +def identify_ams_model_from_module(module: dict[str, Any]) -> str | None: + """Guess the AMS variant that a version module represents.""" + + product_name = (module.get("product_name") or "").strip().lower() + module_name = (module.get("name") or "").strip().lower() + + if "ams lite" in product_name or module_name.startswith("ams_f1"): + return "AMS Lite" + if "ams 2 pro" in product_name or module_name.startswith("n3f"): + return "AMS 2 Pro" + if "ams ht" in product_name or module_name.startswith("ams_ht"): + return "AMS HT" + if module_name == "ams" or module_name.startswith("ams/"): + return "AMS" + + return None + + +def identify_ams_models_from_modules(modules: Iterable[dict[str, Any]]) -> dict[str, dict[str, Any]]: + """Return per-module metadata, including the detected model when available.""" + + results: dict[str, dict[str, Any]] = {} + for module in modules or []: + name = module.get("name") + if not name: + continue + + results[name] = { + "model": identify_ams_model_from_module(module), + "product_name": module.get("product_name"), + "serial": module.get("sn"), + "hw_ver": module.get("hw_ver"), + } + + return results + + +def extract_ams_id_from_module_name(name: str) -> int | None: + parts = name.split("/") + if len(parts) != 2: + return None + try: + return int(parts[1]) + except ValueError: + return None + + +def identify_ams_models_by_id(modules: Iterable[dict[str, Any]]) -> dict[str, str]: + """Return the detected AMS model per numeric AMS ID (module suffix).""" + + results: dict[str, str] = {} + for module in modules or []: + name = module.get("name") + if not name: + continue + + ams_id = extract_ams_id_from_module_name(name) + if ams_id is None: + continue + + model = identify_ams_model_from_module(module) + if model: + results[str(ams_id)] = model + results[ams_id] = model + + return results + + def num2letter(num): return chr(ord("A") + int(num)) @@ -94,45 +173,45 @@ def map_filament(tray_tar): print(f'Filamentchange {len(PENDING_PRINT_METADATA["filamentChanges"])}: Tray {tray_tar}') # Anzahl der erkannten Wechsel - change_count = len(PENDING_PRINT_METADATA["filamentChanges"]) - 1 # -1, weil der erste Eintrag kein Wechsel ist - - filament_order = PENDING_PRINT_METADATA.get("filamentOrder") or {} - ordered_filaments = sorted(filament_order.items(), key=lambda entry: entry[1]) - assigned_trays = PENDING_PRINT_METADATA.setdefault("assigned_trays", []) - filament_assigned = None - if tray_tar not in assigned_trays: - assigned_trays.append(tray_tar) - unique_index = len(assigned_trays) - 1 - if unique_index < len(ordered_filaments): - filament_assigned = ordered_filaments[unique_index][0] - else: - for filamentId, usage_count in filament_order.items(): - if usage_count == change_count: - filament_assigned = filamentId - break - - if filament_assigned is not None: - mapping = PENDING_PRINT_METADATA.setdefault("ams_mapping", []) - filament_idx = int(filament_assigned) - while len(mapping) <= filament_idx: - mapping.append(None) - mapping[filament_idx] = tray_tar - print(f"✅ Tray {tray_tar} assigned to Filament {filament_assigned}") - - for filament, tray in enumerate(mapping): - if tray is None: - continue - print(f" Filament pos: {filament} → Tray {tray}") - - target_filaments = set(filament_order.keys()) - if target_filaments: - assigned_filaments = { - idx for idx, tray in enumerate(PENDING_PRINT_METADATA.get("ams_mapping", [])) - if tray is not None - } - if target_filaments.issubset(assigned_filaments): - print("\n✅ All trays assigned:") - return True + change_count = len(PENDING_PRINT_METADATA["filamentChanges"]) - 1 # -1, weil der erste Eintrag kein Wechsel ist + + filament_order = PENDING_PRINT_METADATA.get("filamentOrder") or {} + ordered_filaments = sorted(filament_order.items(), key=lambda entry: entry[1]) + assigned_trays = PENDING_PRINT_METADATA.setdefault("assigned_trays", []) + filament_assigned = None + if tray_tar not in assigned_trays: + assigned_trays.append(tray_tar) + unique_index = len(assigned_trays) - 1 + if unique_index < len(ordered_filaments): + filament_assigned = ordered_filaments[unique_index][0] + else: + for filamentId, usage_count in filament_order.items(): + if usage_count == change_count: + filament_assigned = filamentId + break + + if filament_assigned is not None: + mapping = PENDING_PRINT_METADATA.setdefault("ams_mapping", []) + filament_idx = int(filament_assigned) + while len(mapping) <= filament_idx: + mapping.append(None) + mapping[filament_idx] = tray_tar + print(f"✅ Tray {tray_tar} assigned to Filament {filament_assigned}") + + for filament, tray in enumerate(mapping): + if tray is None: + continue + print(f" Filament pos: {filament} → Tray {tray}") + + target_filaments = set(filament_order.keys()) + if target_filaments: + assigned_filaments = { + idx for idx, tray in enumerate(PENDING_PRINT_METADATA.get("ams_mapping", [])) + if tray is not None + } + if target_filaments.issubset(assigned_filaments): + print("\n✅ All trays assigned:") + return True return False @@ -161,19 +240,19 @@ def processMessage(data): PENDING_PRINT_METADATA["print_id"] = print_id PENDING_PRINT_METADATA["complete"] = True - for id, filament in PENDING_PRINT_METADATA["filaments"].items(): - parsed_grams = _parse_grams(filament.get("used_g")) - grams_used = parsed_grams if parsed_grams is not None else 0.0 - if TRACK_LAYER_USAGE: - grams_used = 0.0 - insert_filament_usage( - print_id, - filament["type"], - filament["color"], - grams_used, - id, - estimated_grams=parsed_grams, - ) + for id, filament in PENDING_PRINT_METADATA["filaments"].items(): + parsed_grams = _parse_grams(filament.get("used_g")) + grams_used = parsed_grams if parsed_grams is not None else 0.0 + if TRACK_LAYER_USAGE: + grams_used = 0.0 + insert_filament_usage( + print_id, + filament["type"], + filament["color"], + grams_used, + id, + estimated_grams=parsed_grams, + ) #if ("gcode_state" in data["print"] and data["print"]["gcode_state"] == "RUNNING") and ("print_type" in data["print"] and data["print"]["print_type"] != "local") \ # and ("tray_tar" in data["print"] and data["print"]["tray_tar"] != "255") and ("stg_cur" in data["print"] and data["print"]["stg_cur"] == 0 and PRINT_CURRENT_STAGE != 0): @@ -200,23 +279,23 @@ def processMessage(data): if not PENDING_PRINT_METADATA.get("tracking_started"): print_id = insert_print(PENDING_PRINT_METADATA["file"], PRINTER_STATE["print"]["print_type"], PENDING_PRINT_METADATA["image"]) - PENDING_PRINT_METADATA["ams_mapping"] = [] - PENDING_PRINT_METADATA["filamentChanges"] = [] - PENDING_PRINT_METADATA["assigned_trays"] = [] - PENDING_PRINT_METADATA["complete"] = False - PENDING_PRINT_METADATA["print_id"] = print_id + PENDING_PRINT_METADATA["ams_mapping"] = [] + PENDING_PRINT_METADATA["filamentChanges"] = [] + PENDING_PRINT_METADATA["assigned_trays"] = [] + PENDING_PRINT_METADATA["complete"] = False + PENDING_PRINT_METADATA["print_id"] = print_id FILAMENT_TRACKER.start_local_print_from_metadata(PENDING_PRINT_METADATA) - for id, filament in PENDING_PRINT_METADATA["filaments"].items(): - parsed_grams = _parse_grams(filament.get("used_g")) - grams_used = parsed_grams if parsed_grams is not None else 0.0 - if TRACK_LAYER_USAGE: - grams_used = 0.0 - insert_filament_usage( - print_id, - filament["type"], - filament["color"], - grams_used, + for id, filament in PENDING_PRINT_METADATA["filaments"].items(): + parsed_grams = _parse_grams(filament.get("used_g")) + grams_used = parsed_grams if parsed_grams is not None else 0.0 + if TRACK_LAYER_USAGE: + grams_used = 0.0 + insert_filament_usage( + print_id, + filament["type"], + filament["color"], + grams_used, id, estimated_grams=parsed_grams, ) @@ -289,15 +368,32 @@ def processMessage(data): PRINTER_STATE_LAST = copy.deepcopy(PRINTER_STATE) -def publish(client, msg): - result = client.publish(f"device/{PRINTER_ID}/request", json.dumps(msg)) - status = result[0] - if status == 0: - print(f"Sent {msg} to topic device/{PRINTER_ID}/request") - return True - - print(f"Failed to send message to topic device/{PRINTER_ID}/request") - return False +def publish(client, msg): + result = client.publish(f"device/{PRINTER_ID}/request", json.dumps(msg)) + status = result[0] + if status == 0: + print(f"Sent {msg} to topic device/{PRINTER_ID}/request") + return True + + print(f"Failed to send message to topic device/{PRINTER_ID}/request") + return False + + +def clear_ams_tray_assignment(ams_id, tray_id): + if not MQTT_CLIENT: + return + + ams_message = copy.deepcopy(AMS_FILAMENT_SETTING) + ams_message["print"]["ams_id"] = int(ams_id) + ams_message["print"]["tray_id"] = int(tray_id) + ams_message["print"]["tray_color"] = "" + ams_message["print"]["nozzle_temp_min"] = None + ams_message["print"]["nozzle_temp_max"] = None + ams_message["print"]["tray_type"] = "" + ams_message["print"]["setting_id"] = "" + ams_message["print"]["tray_info_idx"] = "" + + publish(MQTT_CLIENT, ams_message) # Inspired by https://github.com/Donkie/Spoolman/issues/217#issuecomment-2303022970 def on_message(client, userdata, msg): @@ -306,6 +402,18 @@ def on_message(client, userdata, msg): try: data = json.loads(msg.payload.decode()) + info = data.get("info") + if info and info.get("command") == "get_version": + modules = info.get("module", []) + detected = identify_ams_models_from_modules(modules) + models_by_id = identify_ams_models_by_id(modules) + LAST_AMS_CONFIG["get_version"] = { + "info": info, + "modules": modules, + "detected_models": detected, + "models_by_id": models_by_id, + } + if "print" in data: append_to_rotating_file("/home/app/logs/mqtt.log", msg.payload.decode()) @@ -351,10 +459,17 @@ def on_message(client, userdata, msg): # "remaining_weight": tray["remain"] / 100 * tray["tray_weight"] # }) - if not found and tray_uuid == "00000000000000000000000000000000": - print(" - No Spool or non Bambulab Spool!") - elif not found: - print(" - Not found. Update spool tag!") + if not found and tray_uuid == "00000000000000000000000000000000": + print(" - No Spool or non Bambulab Spool!") + if CLEAR_ASSIGNMENT_WHEN_EMPTY: + clear_active_spool_for_tray(ams['id'], tray['id']) + clear_ams_tray_assignment(ams['id'], tray['id']) + elif not found: + print(" - Not found. Update spool tag!") + tray["unmapped_bambu_tag"] = tray_uuid + tray["issue"] = True + clear_active_spool_for_tray(ams['id'], tray['id']) + clear_ams_tray_assignment(ams['id'], tray['id']) except Exception: traceback.print_exc() @@ -412,6 +527,12 @@ def getLastAMSConfig(): return LAST_AMS_CONFIG +def getDetectedAmsModelsById(): + global LAST_AMS_CONFIG + detected = LAST_AMS_CONFIG.get("get_version", {}).get("models_by_id") or {} + return dict(detected) + + def getMqttClient(): global MQTT_CLIENT return MQTT_CLIENT diff --git a/spoolman_service.py b/spoolman_service.py index dce13032..8a82bbd2 100644 --- a/spoolman_service.py +++ b/spoolman_service.py @@ -8,9 +8,23 @@ import json import spoolman_client + SPOOLS = {} SPOOLMAN_SETTINGS = {} + +def clear_active_spool_for_tray(ams_id: int, tray_id: int) -> None: + """ + Remove any SpoolMan spool that is currently tagged with the given tray UID. + """ + target = json.dumps(trayUid(ams_id, tray_id)) + for spool in fetchSpools(cached=True): + extras = spool.get("extra") or {} + if extras.get("active_tray") == target: + spoolman_client.patchExtraTags(spool["id"], extras, {"active_tray": json.dumps("")}) + spool.setdefault("extra", {})["active_tray"] = json.dumps("") + break + COLOR_DISTANCE_TOLERANCE = 80 currency_symbols = { @@ -97,7 +111,7 @@ def _log_filament_mismatch(tray_data: dict, spool: dict) -> None: except Exception: pass -def augmentTrayDataWithSpoolMan(spool_list, tray_data, tray_id): +def augmentTrayDataWithSpoolMan(spool_list, tray_data, ams_id, tray_id): tray_data["matched"] = False tray_data["mismatch"] = False tray_data["issue"] = False @@ -105,6 +119,13 @@ def augmentTrayDataWithSpoolMan(spool_list, tray_data, tray_id): tray_data["color_mismatch_message"] = "" tray_data["ams_material_missing"] = False tray_data["ams_material_missing_message"] = "" + tray_data["unmapped_bambu_tag"] = "" + tray_data["bambu_material"] = "" + tray_data["bambu_color"] = "" + tray_data["bambu_vendor"] = "" + tray_data["bambu_sub_brand"] = "" + tray_sub_brands_raw = "" + tray_data["spool_id"] = None def _clean_basic(val: str) -> str: # Normalization for matching: @@ -134,12 +155,18 @@ def _clean_basic(val: str) -> str: "tray_sub_brand", ]: tray_data.pop(field, None) + tray_data["spool_id"] = None return + tray_uuid = str(tray_data.get("tray_uuid") or "") + tray_uid = trayUid(ams_id, tray_id) + target_active_tray = json.dumps(tray_uid) + for spool in spool_list: - if spool.get("extra") and spool["extra"].get("active_tray") and spool["extra"]["active_tray"] == json.dumps(tray_id): + if spool.get("extra") and spool["extra"].get("active_tray") and spool["extra"]["active_tray"] == target_active_tray: tray_data["name"] = spool["filament"]["name"] tray_data["vendor"] = spool["filament"]["vendor"]["name"] + tray_data["spool_id"] = spool["id"] tray_data["spool_material"] = spool["filament"].get("material", "") tray_data["spool_sub_brand"] = (spool["filament"].get("extra", {}).get("type") or "").replace('"', '').strip() tray_data["remaining_weight"] = spool["remaining_weight"] @@ -272,6 +299,20 @@ def _clean_basic(val: str) -> str: if tray_data.get("tray_type") and tray_data["tray_type"] != "" and not tray_data.get("matched", False): tray_data["issue"] = True + if not tray_data["matched"] and tray_uuid and tray_uuid != "00000000000000000000000000000000": + tray_data["unmapped_bambu_tag"] = tray_uuid + tray_data["bambu_material"] = (tray_data.get("tray_type") or "").strip() + tray_data["bambu_color"] = normalize_color_hex(tray_data.get("tray_color") or "") + tray_data["bambu_vendor"] = "Bambu Lab" + tray_data["bambu_sub_brand"] = tray_sub_brands_raw.strip() + clear_active_spool_for_tray(ams_id, tray_id) + else: + tray_data["unmapped_bambu_tag"] = "" + tray_data["bambu_material"] = "" + tray_data["bambu_color"] = "" + tray_data["bambu_vendor"] = "" + tray_data["bambu_sub_brand"] = "" + def spendFilaments(printdata): if printdata["ams_mapping"]: ams_mapping = printdata["ams_mapping"] diff --git a/templates/assign_bambu_spool.html b/templates/assign_bambu_spool.html new file mode 100644 index 00000000..bb7a286f --- /dev/null +++ b/templates/assign_bambu_spool.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} + +{% block content %} +
Bambu tag {{ bambu_tag }} detected in this tray. Pick the matching SpoolMan spool to bind this tag and assign the tray.