mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 03:01:21 +02:00
[Fix]: Send Bambu RFID Tags to Spoolman, Allow Unlink in Manual Mode, Stop Location Clearing for Generic Spools (#719)
[Fix]: Send Bambu RFID Tags to Spoolman, Allow Unlink in Manual Mode, Stop Location Clearing for Generic Spools (#719)
This commit is contained in:
@@ -9,6 +9,7 @@ import httpx
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
BAMBU_RFID_TAG_LENGTH = 32
|
||||
|
||||
@dataclass
|
||||
class SpoolmanSpool:
|
||||
@@ -553,6 +554,10 @@ class SpoolmanClient:
|
||||
else:
|
||||
spool_uuid = ""
|
||||
|
||||
# Only clear location for Bambu Lab spools (those with a stored 32-character RFID tag).
|
||||
if len(spool_uuid) != BAMBU_RFID_TAG_LENGTH:
|
||||
continue
|
||||
|
||||
# If this spool's UUID is not in the current AMS, clear its location
|
||||
if spool_uuid not in current_tray_uuids:
|
||||
logger.info(
|
||||
|
||||
@@ -298,11 +298,15 @@ class TestSpoolmanClient:
|
||||
async def test_clear_location_for_removed_spools_with_cached_spools(self, client):
|
||||
"""Verify clear_location_for_removed_spools uses cached spools."""
|
||||
cached = [
|
||||
{"id": 1, "location": "Printer1 - AMS A1", "extra": {"tag": '"TAG1"'}},
|
||||
{"id": 2, "location": "Printer1 - AMS A2", "extra": {"tag": '"TAG2"'}},
|
||||
{"id": 3, "location": "Printer1 - AMS A3", "extra": {"tag": '"TAG3"'}},
|
||||
{"id": 1, "location": "Printer1 - AMS A1", "extra": {"tag": '"A1B2C3D4E5F60718293A4B5C6D7E8F90"'}},
|
||||
{"id": 2, "location": "Printer1 - AMS A2", "extra": {"tag": '"B1C2D3E4F5061728394A5B6C7D8E9F01"'}},
|
||||
{"id": 3, "location": "Printer1 - AMS A3", "extra": {"tag": '"C1D2E3F40516273849A5B6C7D8E9F012"'}},
|
||||
]
|
||||
current_tags = {"TAG1", "TAG2"} # TAG3 was removed
|
||||
# Tag 3 was cleared, so only tags 1 and 2 are current
|
||||
current_tags = {
|
||||
"A1B2C3D4E5F60718293A4B5C6D7E8F90",
|
||||
"B1C2D3E4F5061728394A5B6C7D8E9F01",
|
||||
}
|
||||
|
||||
with (
|
||||
patch.object(client, "get_spools", AsyncMock()) as mock_get,
|
||||
|
||||
@@ -117,7 +117,7 @@ describe('LinkSpoolModal', () => {
|
||||
});
|
||||
|
||||
describe('linking', () => {
|
||||
it('calls linkSpool on spool click', async () => {
|
||||
it('uses trayUuid when linking if present (Bambu spool path)', async () => {
|
||||
render(<LinkSpoolModal {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -126,6 +126,25 @@ describe('LinkSpoolModal', () => {
|
||||
|
||||
fireEvent.click(screen.getByText(/Generic PLA Red/).closest('button')!);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(api.linkSpool).toHaveBeenCalledWith(1, {
|
||||
spoolTag: 'A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4',
|
||||
printerId: 1,
|
||||
amsId: 0,
|
||||
trayId: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('falls back to tagUid when trayUuid is missing (generic spool path)', async () => {
|
||||
render(<LinkSpoolModal {...defaultProps} trayUuid="" />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Generic PLA Red/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText(/Generic PLA Red/).closest('button')!);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(api.linkSpool).toHaveBeenCalledWith(1, {
|
||||
spoolTag: 'ABCD1234',
|
||||
|
||||
@@ -21,6 +21,7 @@ interface SpoolmanConfig {
|
||||
onUnlinkSpool?: () => void;
|
||||
linkedSpoolId?: number | null; // Spoolman spool ID if this tray is already linked
|
||||
spoolmanUrl?: string | null; // Base URL for Spoolman (for "Open in Spoolman" link)
|
||||
syncMode?: string | null; // If auto-sync is enabled, we may want to hide the unlink option for Bambu spools
|
||||
}
|
||||
|
||||
interface InventoryConfig {
|
||||
@@ -305,7 +306,7 @@ export function FilamentHoverCard({ data, children, disabled, className = '', sp
|
||||
{t('spoolman.openInSpoolman')}
|
||||
</a>
|
||||
|
||||
{spoolman.onUnlinkSpool && data.vendor !== 'Bambu Lab' && (
|
||||
{spoolman.onUnlinkSpool && (data.vendor !== 'Bambu Lab' || spoolman.syncMode === 'manual') && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
@@ -22,7 +22,7 @@ export function LinkSpoolModal({ isOpen, onClose, tagUid, trayUuid, printerId, a
|
||||
const queryClient = useQueryClient();
|
||||
const { showToast } = useToast();
|
||||
const [search, setSearch] = useState('');
|
||||
const spoolTag = tagUid || trayUuid;
|
||||
const spoolTag = trayUuid || tagUid;
|
||||
|
||||
const { data: spools, isLoading } = useQuery({
|
||||
queryKey: ['unlinked-spools'],
|
||||
@@ -97,9 +97,9 @@ export function LinkSpoolModal({ isOpen, onClose, tagUid, trayUuid, printerId, a
|
||||
className="w-full pl-9 pr-3 py-2 bg-bambu-dark rounded-lg border border-white/10 text-white text-sm placeholder:text-bambu-gray focus:outline-none focus:border-bambu-green"
|
||||
/>
|
||||
</div>
|
||||
{(tagUid || trayUuid) && (
|
||||
<p className="text-xs text-bambu-gray mt-2 font-mono truncate" title={tagUid || trayUuid}>
|
||||
Tag: {tagUid || trayUuid}
|
||||
{(trayUuid || tagUid) && (
|
||||
<p className="text-xs text-bambu-gray mt-2 font-mono truncate" title={trayUuid || tagUid}>
|
||||
Tag: {trayUuid || tagUid}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1530,6 +1530,7 @@ function PrinterCard({
|
||||
spoolmanEnabled = false,
|
||||
linkedSpools,
|
||||
spoolmanUrl,
|
||||
spoolmanSyncMode,
|
||||
onGetAssignment,
|
||||
onUnassignSpool,
|
||||
timeFormat = 'system',
|
||||
@@ -1553,6 +1554,7 @@ function PrinterCard({
|
||||
hasUnlinkedSpools?: boolean;
|
||||
linkedSpools?: Record<string, LinkedSpoolInfo>;
|
||||
spoolmanUrl?: string | null;
|
||||
spoolmanSyncMode?: string | null;
|
||||
spoolAssignments?: SpoolAssignment[];
|
||||
onGetAssignment?: (printerId: number, amsId: number, trayId: number) => SpoolAssignment | undefined;
|
||||
onUnassignSpool?: (printerId: number, amsId: number, trayId: number) => void;
|
||||
@@ -3176,7 +3178,7 @@ function PrinterCard({
|
||||
const slotPreset = slotPresets?.[globalTrayId];
|
||||
|
||||
// Fill level fallback chain: Spoolman → Inventory → AMS remain
|
||||
const trayTag = (tray?.tag_uid || tray?.tray_uuid || getFallbackSpoolTag(printer.serial_number, ams.id, slotIdx))?.toUpperCase();
|
||||
const trayTag = (tray?.tray_uuid || tray?.tag_uid || getFallbackSpoolTag(printer.serial_number, ams.id, slotIdx))?.toUpperCase();
|
||||
const linkedSpool = trayTag ? linkedSpools?.[trayTag] : undefined;
|
||||
const spoolmanFill = getSpoolmanFillLevel(linkedSpool);
|
||||
const inventoryAssignment = onGetAssignment?.(printer.id, ams.id, slotIdx);
|
||||
@@ -3303,6 +3305,7 @@ function PrinterCard({
|
||||
? linkedSpools?.[trayTag]?.id
|
||||
: undefined,
|
||||
spoolmanUrl,
|
||||
syncMode: spoolmanSyncMode,
|
||||
onLinkSpool: spoolmanEnabled ? () => {
|
||||
const linkTag = (filamentData.trayUuid || filamentData.tagUid || getFallbackSpoolTag(printer.serial_number, ams.id, slotIdx)).toUpperCase();
|
||||
setLinkSpoolModal({
|
||||
@@ -3619,6 +3622,7 @@ function PrinterCard({
|
||||
? linkedSpools?.[htTrayTag]?.id
|
||||
: undefined,
|
||||
spoolmanUrl,
|
||||
syncMode: spoolmanSyncMode,
|
||||
onLinkSpool: spoolmanEnabled ? () => {
|
||||
const linkTag = (filamentData.trayUuid || filamentData.tagUid || getFallbackSpoolTag(printer.serial_number, ams.id, htSlotId)).toUpperCase();
|
||||
setLinkSpoolModal({
|
||||
@@ -3832,6 +3836,7 @@ function PrinterCard({
|
||||
? linkedSpools?.[extTrayTag]?.id
|
||||
: undefined,
|
||||
spoolmanUrl,
|
||||
syncMode: spoolmanSyncMode,
|
||||
onLinkSpool: spoolmanEnabled ? () => {
|
||||
const linkTag = (extFilamentData.trayUuid || extFilamentData.tagUid || getFallbackSpoolTag(printer.serial_number, 255, slotTrayId)).toUpperCase();
|
||||
setLinkSpoolModal({
|
||||
@@ -5715,6 +5720,15 @@ export function PrintersPage() {
|
||||
});
|
||||
const spoolmanEnabled = spoolmanStatus?.enabled && spoolmanStatus?.connected;
|
||||
|
||||
// Fetch Spoolman settings to get sync mode
|
||||
const { data: spoolmanSettings } = useQuery({
|
||||
queryKey: ['spoolman-settings'],
|
||||
queryFn: api.getSpoolmanSettings,
|
||||
enabled: !!spoolmanEnabled,
|
||||
staleTime: 60 * 1000, // 1 minute
|
||||
});
|
||||
const spoolmanSyncMode = spoolmanSettings?.spoolman_sync_mode;
|
||||
|
||||
// Fetch unlinked spools to know if link button should be enabled
|
||||
const { data: unlinkedSpools } = useQuery({
|
||||
queryKey: ['unlinked-spools'],
|
||||
@@ -6064,6 +6078,7 @@ export function PrintersPage() {
|
||||
hasUnlinkedSpools={hasUnlinkedSpools}
|
||||
linkedSpools={linkedSpools}
|
||||
spoolmanUrl={spoolmanStatus?.url}
|
||||
spoolmanSyncMode={spoolmanSyncMode}
|
||||
onGetAssignment={getAssignment}
|
||||
onUnassignSpool={(pid, aid, tid) => unassignMutation.mutate({ printerId: pid, amsId: aid, trayId: tid })}
|
||||
timeFormat={settings?.time_format || 'system'}
|
||||
@@ -6092,6 +6107,7 @@ export function PrintersPage() {
|
||||
hasUnlinkedSpools={hasUnlinkedSpools}
|
||||
linkedSpools={linkedSpools}
|
||||
spoolmanUrl={spoolmanStatus?.url}
|
||||
spoolmanSyncMode={spoolmanSyncMode}
|
||||
onGetAssignment={getAssignment}
|
||||
onUnassignSpool={(pid, aid, tid) => unassignMutation.mutate({ printerId: pid, amsId: aid, trayId: tid })}
|
||||
amsThresholds={settings ? {
|
||||
|
||||
Reference in New Issue
Block a user