slot_preset_writer.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. """Shared upsert for the slot_preset_mappings row that drives the AMS slot
  2. card's displayed preset name.
  3. Three call sites must keep this row in sync with the currently-assigned spool:
  4. - ``api.routes.inventory.apply_spool_to_slot_via_mqtt`` (internal manual assign)
  5. - ``services.spool_tag_matcher.auto_assign_spool`` (internal RFID auto-assign)
  6. - ``main.auto_sync_spoolman_ams_trays`` (Spoolman RFID-driven sync)
  7. If any of them skips this row, the slot card surfaces the previous spool's
  8. preset name because the PrintersPage display chain consults
  9. slot_preset_mappings.preset_name first — it overrides cloudInfo.name and the
  10. spool's own slicer_filament_name.
  11. """
  12. from __future__ import annotations
  13. import logging
  14. from sqlalchemy import select
  15. from sqlalchemy.ext.asyncio import AsyncSession
  16. from backend.app.models.slot_preset import SlotPresetMapping
  17. from backend.app.models.spool import Spool
  18. from backend.app.utils.filament_ids import filament_id_to_setting_id
  19. logger = logging.getLogger(__name__)
  20. async def upsert_slot_preset(
  21. *,
  22. db: AsyncSession,
  23. printer_id: int,
  24. ams_id: int,
  25. tray_id: int,
  26. preset_id: str,
  27. preset_name: str,
  28. preset_source: str = "cloud",
  29. configured_tray_info_idx: str | None = None,
  30. ) -> None:
  31. """Primitive upsert. No-op when ``preset_id`` is empty (the column is
  32. NOT NULL on the model, and an empty string isn't a useful key to
  33. overwrite by). Soft-fails on DB errors so a broken upsert never
  34. cascades into the surrounding spool-assign flow.
  35. ``configured_tray_info_idx`` is the filament id the slot was given along
  36. with this preset, when the caller knows it; the slot card stops trusting
  37. the row once the printer reports a different one (#3216). It is replaced
  38. on every write, so a stale id never outlives the preset it came with.
  39. """
  40. if not preset_id:
  41. return
  42. try:
  43. existing = await db.execute(
  44. select(SlotPresetMapping).where(
  45. SlotPresetMapping.printer_id == printer_id,
  46. SlotPresetMapping.ams_id == ams_id,
  47. SlotPresetMapping.tray_id == tray_id,
  48. )
  49. )
  50. mapping = existing.scalar_one_or_none()
  51. if mapping:
  52. mapping.preset_id = preset_id
  53. mapping.preset_name = preset_name
  54. mapping.preset_source = preset_source
  55. mapping.tray_info_idx = configured_tray_info_idx or None
  56. else:
  57. mapping = SlotPresetMapping(
  58. printer_id=printer_id,
  59. ams_id=ams_id,
  60. tray_id=tray_id,
  61. preset_id=preset_id,
  62. preset_name=preset_name,
  63. preset_source=preset_source,
  64. tray_info_idx=configured_tray_info_idx or None,
  65. )
  66. db.add(mapping)
  67. await db.commit()
  68. except Exception as e:
  69. logger.warning(
  70. "Failed to save slot preset mapping for printer=%d ams=%d tray=%d: %s",
  71. printer_id,
  72. ams_id,
  73. tray_id,
  74. e,
  75. )
  76. async def upsert_slot_preset_for_spool(
  77. *,
  78. db: AsyncSession,
  79. spool: Spool,
  80. printer_id: int,
  81. ams_id: int,
  82. tray_id: int,
  83. tray_info_idx: str = "",
  84. tray_sub_brands: str = "",
  85. tray_type: str = "",
  86. setting_id: str = "",
  87. configured_tray_info_idx: str | None = None,
  88. ) -> None:
  89. """Convenience wrapper for internal-mode call sites — derives the
  90. (preset_id, preset_name, preset_source) triple from a ``Spool`` ORM object,
  91. then defers to ``upsert_slot_preset``.
  92. Local numeric ``spool.slicer_filament`` (e.g. ``"50"``) → ``local_50``;
  93. cloud-form ids (GFS… / GFA… via ``filament_id_to_setting_id`` on the
  94. tray's ``tray_info_idx``) → standard setting_id form.
  95. """
  96. preset_name = spool.slicer_filament_name or tray_sub_brands or tray_type
  97. preset_source = "cloud"
  98. sf = spool.slicer_filament or ""
  99. if sf:
  100. base_sf_mapping = sf.split("_")[0] if "_" in sf else sf
  101. try:
  102. int(base_sf_mapping)
  103. preset_id = f"local_{base_sf_mapping}"
  104. preset_source = "local"
  105. except (ValueError, TypeError):
  106. preset_id = filament_id_to_setting_id(tray_info_idx) if tray_info_idx else setting_id
  107. else:
  108. preset_id = filament_id_to_setting_id(tray_info_idx) if tray_info_idx else ""
  109. await upsert_slot_preset(
  110. db=db,
  111. printer_id=printer_id,
  112. ams_id=ams_id,
  113. tray_id=tray_id,
  114. preset_id=preset_id,
  115. preset_name=preset_name,
  116. preset_source=preset_source,
  117. configured_tray_info_idx=configured_tray_info_idx,
  118. )
  119. async def upsert_slot_preset_for_spoolman_spool(
  120. *,
  121. db: AsyncSession,
  122. spoolman_spool: dict,
  123. tray_info_idx: str,
  124. tray_sub_brands: str,
  125. tray_type: str,
  126. printer_id: int,
  127. ams_id: int,
  128. tray_id: int,
  129. ) -> None:
  130. """Spoolman-mode counterpart. Derives ``preset_name`` from the Spoolman
  131. filament's display name (falling back to material if absent) and
  132. ``preset_id`` from the AMS-reported tray_info_idx (the cloud filament
  133. id the printer is currently using). ``preset_source`` is always
  134. ``"cloud"`` since Spoolman doesn't carry a local-preset concept.
  135. The ``spoolman_spool`` dict is the shape returned by
  136. ``SpoolmanClient.sync_ams_tray`` — ``spool["filament"]["name"]`` etc.
  137. """
  138. filament = spoolman_spool.get("filament") or {}
  139. preset_name = filament.get("name") or filament.get("material") or tray_sub_brands or tray_type
  140. preset_id = filament_id_to_setting_id(tray_info_idx) if tray_info_idx else ""
  141. await upsert_slot_preset(
  142. db=db,
  143. printer_id=printer_id,
  144. ams_id=ams_id,
  145. tray_id=tray_id,
  146. preset_id=preset_id,
  147. preset_name=preset_name or "",
  148. preset_source="cloud",
  149. # The AMS-reported id this row was derived from.
  150. configured_tray_info_idx=tray_info_idx,
  151. )