slot_preset_writer.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  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. ) -> None:
  30. """Primitive upsert. No-op when ``preset_id`` is empty (the column is
  31. NOT NULL on the model, and an empty string isn't a useful key to
  32. overwrite by). Soft-fails on DB errors so a broken upsert never
  33. cascades into the surrounding spool-assign flow.
  34. """
  35. if not preset_id:
  36. return
  37. try:
  38. existing = await db.execute(
  39. select(SlotPresetMapping).where(
  40. SlotPresetMapping.printer_id == printer_id,
  41. SlotPresetMapping.ams_id == ams_id,
  42. SlotPresetMapping.tray_id == tray_id,
  43. )
  44. )
  45. mapping = existing.scalar_one_or_none()
  46. if mapping:
  47. mapping.preset_id = preset_id
  48. mapping.preset_name = preset_name
  49. mapping.preset_source = preset_source
  50. else:
  51. mapping = SlotPresetMapping(
  52. printer_id=printer_id,
  53. ams_id=ams_id,
  54. tray_id=tray_id,
  55. preset_id=preset_id,
  56. preset_name=preset_name,
  57. preset_source=preset_source,
  58. )
  59. db.add(mapping)
  60. await db.commit()
  61. except Exception as e:
  62. logger.warning(
  63. "Failed to save slot preset mapping for printer=%d ams=%d tray=%d: %s",
  64. printer_id,
  65. ams_id,
  66. tray_id,
  67. e,
  68. )
  69. async def upsert_slot_preset_for_spool(
  70. *,
  71. db: AsyncSession,
  72. spool: Spool,
  73. printer_id: int,
  74. ams_id: int,
  75. tray_id: int,
  76. tray_info_idx: str = "",
  77. tray_sub_brands: str = "",
  78. tray_type: str = "",
  79. setting_id: str = "",
  80. ) -> None:
  81. """Convenience wrapper for internal-mode call sites — derives the
  82. (preset_id, preset_name, preset_source) triple from a ``Spool`` ORM object,
  83. then defers to ``upsert_slot_preset``.
  84. Local numeric ``spool.slicer_filament`` (e.g. ``"50"``) → ``local_50``;
  85. cloud-form ids (GFS… / GFA… via ``filament_id_to_setting_id`` on the
  86. tray's ``tray_info_idx``) → standard setting_id form.
  87. """
  88. preset_name = spool.slicer_filament_name or tray_sub_brands or tray_type
  89. preset_source = "cloud"
  90. sf = spool.slicer_filament or ""
  91. if sf:
  92. base_sf_mapping = sf.split("_")[0] if "_" in sf else sf
  93. try:
  94. int(base_sf_mapping)
  95. preset_id = f"local_{base_sf_mapping}"
  96. preset_source = "local"
  97. except (ValueError, TypeError):
  98. preset_id = filament_id_to_setting_id(tray_info_idx) if tray_info_idx else setting_id
  99. else:
  100. preset_id = filament_id_to_setting_id(tray_info_idx) if tray_info_idx else ""
  101. await upsert_slot_preset(
  102. db=db,
  103. printer_id=printer_id,
  104. ams_id=ams_id,
  105. tray_id=tray_id,
  106. preset_id=preset_id,
  107. preset_name=preset_name,
  108. preset_source=preset_source,
  109. )
  110. async def upsert_slot_preset_for_spoolman_spool(
  111. *,
  112. db: AsyncSession,
  113. spoolman_spool: dict,
  114. tray_info_idx: str,
  115. tray_sub_brands: str,
  116. tray_type: str,
  117. printer_id: int,
  118. ams_id: int,
  119. tray_id: int,
  120. ) -> None:
  121. """Spoolman-mode counterpart. Derives ``preset_name`` from the Spoolman
  122. filament's display name (falling back to material if absent) and
  123. ``preset_id`` from the AMS-reported tray_info_idx (the cloud filament
  124. id the printer is currently using). ``preset_source`` is always
  125. ``"cloud"`` since Spoolman doesn't carry a local-preset concept.
  126. The ``spoolman_spool`` dict is the shape returned by
  127. ``SpoolmanClient.sync_ams_tray`` — ``spool["filament"]["name"]`` etc.
  128. """
  129. filament = spoolman_spool.get("filament") or {}
  130. preset_name = filament.get("name") or filament.get("material") or tray_sub_brands or tray_type
  131. preset_id = filament_id_to_setting_id(tray_info_idx) if tray_info_idx else ""
  132. await upsert_slot_preset(
  133. db=db,
  134. printer_id=printer_id,
  135. ams_id=ams_id,
  136. tray_id=tray_id,
  137. preset_id=preset_id,
  138. preset_name=preset_name or "",
  139. preset_source="cloud",
  140. )