obico_actions.py 4.3 KB

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