obico_actions.py 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. """Action dispatch for Obico failure detection.
  2. Separated from the detection loop so actions can be unit-tested and swapped.
  3. """
  4. import logging
  5. from sqlalchemy import select
  6. from backend.app.core.database import async_session
  7. from backend.app.models.printer import Printer
  8. logger = logging.getLogger(__name__)
  9. async def execute_action(
  10. printer_id: int,
  11. action: str,
  12. task_name: str,
  13. score: float | None,
  14. frame: bytes | None = None,
  15. *,
  16. print_quality: int | None = None,
  17. printer_name: str | None = None,
  18. ) -> None:
  19. """Run the configured action for a detected print failure.
  20. action: 'notify' | 'pause' | 'pause_and_off'
  21. frame: the camera snapshot the ML model flagged, if any — attached to
  22. the notification so the recipient can see what triggered it.
  23. """
  24. # OctoEverywhere resolves the name before committing an action so stale
  25. # checks can still be cancelled before any hardware control begins.
  26. if printer_name is None:
  27. printer_name = await _get_printer_name(printer_id)
  28. if action in ("pause", "pause_and_off"):
  29. _pause_print(printer_id)
  30. if action == "pause_and_off":
  31. await _turn_off_linked_plugs(printer_id)
  32. await _notify(printer_id, printer_name, task_name, score, action, frame, print_quality=print_quality)
  33. async def _get_printer_name(printer_id: int) -> str:
  34. async with async_session() as db:
  35. result = await db.execute(select(Printer).where(Printer.id == printer_id))
  36. printer = result.scalar_one_or_none()
  37. return printer.name if printer else f"Printer {printer_id}"
  38. def _pause_print(printer_id: int) -> None:
  39. from backend.app.services.printer_manager import printer_manager
  40. client = printer_manager.get_client(printer_id)
  41. if not client:
  42. logger.warning("AI failure detection: no MQTT client for printer %s", printer_id)
  43. return
  44. if not client.pause_print():
  45. logger.warning("AI failure detection: pause_print() returned False for printer %s", printer_id)
  46. async def _turn_off_linked_plugs(printer_id: int) -> None:
  47. from backend.app.services.smart_plug_manager import (
  48. _is_script_plug,
  49. publish_plug_state,
  50. run_linked_scripts_after_automatic_switch,
  51. smart_plug_manager,
  52. )
  53. power_plug_off = None
  54. async with async_session() as db:
  55. plugs = await smart_plug_manager._get_plugs_for_printer(printer_id, db)
  56. for plug in plugs:
  57. # A script can only be run, not turned off; the "printer turns
  58. # off" ones run below, after the plug that feeds the printer.
  59. if not plug.enabled or _is_script_plug(plug):
  60. continue
  61. try:
  62. service = await smart_plug_manager.get_service_for_plug(plug, db)
  63. if await service.turn_off(plug):
  64. await publish_plug_state(
  65. plug.id, plug.name, plug.printer_id, on=False, ha_entity_id=plug.ha_entity_id
  66. )
  67. if plug.controls_printer_power:
  68. power_plug_off = plug
  69. logger.info("AI failure detection: turned off plug %s for printer %s", plug.name, printer_id)
  70. except Exception as e:
  71. logger.error("AI failure detection: failed to turn off plug %s: %s", plug.name, e)
  72. if power_plug_off is not None:
  73. # Once, however many plugs feed the printer.
  74. await run_linked_scripts_after_automatic_switch(printer_id, power_plug_off, "OFF")
  75. async def _notify(
  76. printer_id: int,
  77. printer_name: str,
  78. task_name: str,
  79. score: float | None,
  80. action: str,
  81. frame: bytes | None = None,
  82. *,
  83. print_quality: int | None = None,
  84. ) -> None:
  85. """Fire the AI Failure Detection notification (#1794).
  86. Routed to its own event in 0.2.5b1; previously rode the multiplexed
  87. on_printer_error toggle, which made it indistinguishable from HMS
  88. hardware errors in the UI.
  89. """
  90. from backend.app.services.notification_service import notification_service
  91. async with async_session() as db:
  92. try:
  93. await notification_service.on_ai_failure_detection(
  94. printer_id=printer_id,
  95. printer_name=printer_name,
  96. task_name=task_name,
  97. confidence=score,
  98. action=action,
  99. db=db,
  100. print_quality=print_quality,
  101. image_data=frame,
  102. )
  103. except Exception as e:
  104. logger.error("AI failure detection notify failed for printer %s: %s", printer_id, e)