From 0caa68b77f8f9c32f053bbb545f5c791b263985f Mon Sep 17 00:00:00 2001 From: maziggy Date: Thu, 22 Jan 2026 09:23:40 +0100 Subject: [PATCH] Auto-create Spoolman 'tag' extra field on connect (Issue #123) The 'tag' extra field is required in Spoolman for storing RFID/UUID identifiers that link Bambu Lab spools to Spoolman entries. Previously, users had to manually create this field in Spoolman, causing sync failures for fresh installations. Added ensure_tag_extra_field() method to SpoolmanClient that: - Checks if the 'tag' field exists via GET /api/v1/field/spool/tag - Creates it via POST if missing The method is called automatically: - On app startup when auto-connecting to Spoolman - When user clicks "Connect" in Spoolman settings --- CHANGELOG.md | 1 + backend/app/api/routes/spoolman.py | 3 ++ backend/app/main.py | 2 ++ backend/app/services/spoolman.py | 36 +++++++++++++++++++ .../tests/integration/test_spoolman_api.py | 1 + 5 files changed, 43 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3ae9ff90..df3f793ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ All notable changes to Bambuddy will be documented in this file. ### Fixed - **Filament cost using wrong default** - Statistics now correctly uses the "Default filament cost (per kg)" setting instead of hardcoded €25 value (Issue #120) +- **Spoolman tag field not auto-created** - The required "tag" extra field is now automatically created in Spoolman on first connect, fixing sync failures for fresh Spoolman installs (Issue #123) ## [0.1.6b10] - 2026-01-21 diff --git a/backend/app/api/routes/spoolman.py b/backend/app/api/routes/spoolman.py index 698b6b0f7..437e2a5bb 100644 --- a/backend/app/api/routes/spoolman.py +++ b/backend/app/api/routes/spoolman.py @@ -109,6 +109,9 @@ async def connect_spoolman(db: AsyncSession = Depends(get_db)): detail=f"Could not connect to Spoolman at {url}", ) + # Ensure the 'tag' extra field exists for RFID/UUID storage + await client.ensure_tag_extra_field() + return {"success": True, "message": f"Connected to Spoolman at {url}"} except Exception as e: logger.error(f"Failed to connect to Spoolman: {e}") diff --git a/backend/app/main.py b/backend/app/main.py index f9041af52..8e0feeacc 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1903,6 +1903,8 @@ async def lifespan(app: FastAPI): client = await init_spoolman_client(spoolman_url) if await client.health_check(): logging.info(f"Auto-connected to Spoolman at {spoolman_url}") + # Ensure the 'tag' extra field exists for RFID/UUID storage + await client.ensure_tag_extra_field() else: logging.warning(f"Spoolman at {spoolman_url} is not reachable") except Exception as e: diff --git a/backend/app/services/spoolman.py b/backend/app/services/spoolman.py index fd452b967..3be407df1 100644 --- a/backend/app/services/spoolman.py +++ b/backend/app/services/spoolman.py @@ -488,6 +488,42 @@ class SpoolmanClient: vendor = await self.create_vendor("Bambu Lab") return vendor["id"] if vendor else None + async def ensure_tag_extra_field(self) -> bool: + """Ensure the 'tag' extra field exists for spools. + + Spoolman requires extra fields to be registered before use. + This creates the 'tag' field used to store RFID/UUID identifiers. + + Returns: + True if field exists or was created, False on failure. + """ + try: + client = await self._get_client() + + # Check if field already exists + response = await client.get(f"{self.api_url}/field/spool/tag") + if response.status_code == 200: + logger.debug("Spoolman 'tag' extra field already exists") + return True + + # Field doesn't exist - create it + field_data = { + "name": "tag", + "field_type": "text", + "default_value": None, + } + response = await client.post(f"{self.api_url}/field/spool/tag", json=field_data) + if response.status_code in (200, 201): + logger.info("Created 'tag' extra field in Spoolman") + return True + + logger.warning(f"Failed to create 'tag' extra field: {response.status_code} - {response.text}") + return False + + except Exception as e: + logger.warning(f"Failed to ensure 'tag' extra field exists: {e}") + return False + def parse_ams_tray(self, ams_id: int, tray_data: dict) -> AMSTray | None: """Parse AMS tray data into AMSTray object. diff --git a/backend/tests/integration/test_spoolman_api.py b/backend/tests/integration/test_spoolman_api.py index f22c54bad..fab955e55 100644 --- a/backend/tests/integration/test_spoolman_api.py +++ b/backend/tests/integration/test_spoolman_api.py @@ -39,6 +39,7 @@ class TestSpoolmanAPI: mock_client.is_connected = True mock_client.base_url = "http://localhost:7912" mock_client.health_check = AsyncMock(return_value=True) + mock_client.ensure_tag_extra_field = AsyncMock(return_value=True) mock_client.get_spools = AsyncMock(return_value=[]) mock_client.get_filaments = AsyncMock(return_value=[]) mock_client.create_spool = AsyncMock(return_value={"id": 1})