smart_plugs.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. """API routes for smart plug management."""
  2. import logging
  3. from datetime import datetime
  4. from fastapi import APIRouter, Depends, HTTPException, Query
  5. from sqlalchemy.ext.asyncio import AsyncSession
  6. from sqlalchemy import select
  7. from backend.app.core.database import get_db
  8. from backend.app.models.smart_plug import SmartPlug
  9. from backend.app.models.printer import Printer
  10. from backend.app.schemas.smart_plug import (
  11. SmartPlugCreate,
  12. SmartPlugUpdate,
  13. SmartPlugResponse,
  14. SmartPlugControl,
  15. SmartPlugStatus,
  16. SmartPlugTestConnection,
  17. )
  18. from backend.app.services.tasmota import tasmota_service
  19. logger = logging.getLogger(__name__)
  20. router = APIRouter(prefix="/smart-plugs", tags=["smart-plugs"])
  21. @router.get("/", response_model=list[SmartPlugResponse])
  22. async def list_smart_plugs(db: AsyncSession = Depends(get_db)):
  23. """List all smart plugs."""
  24. result = await db.execute(select(SmartPlug).order_by(SmartPlug.name))
  25. return list(result.scalars().all())
  26. @router.post("/", response_model=SmartPlugResponse)
  27. async def create_smart_plug(
  28. data: SmartPlugCreate,
  29. db: AsyncSession = Depends(get_db),
  30. ):
  31. """Create a new smart plug."""
  32. # Validate printer_id if provided
  33. if data.printer_id:
  34. result = await db.execute(
  35. select(Printer).where(Printer.id == data.printer_id)
  36. )
  37. if not result.scalar_one_or_none():
  38. raise HTTPException(400, "Printer not found")
  39. # Check if printer already has a plug assigned
  40. result = await db.execute(
  41. select(SmartPlug).where(SmartPlug.printer_id == data.printer_id)
  42. )
  43. if result.scalar_one_or_none():
  44. raise HTTPException(400, "This printer already has a smart plug assigned")
  45. plug = SmartPlug(**data.model_dump())
  46. db.add(plug)
  47. await db.commit()
  48. await db.refresh(plug)
  49. logger.info(f"Created smart plug '{plug.name}' at {plug.ip_address}")
  50. return plug
  51. @router.get("/{plug_id}", response_model=SmartPlugResponse)
  52. async def get_smart_plug(plug_id: int, db: AsyncSession = Depends(get_db)):
  53. """Get a specific smart plug."""
  54. result = await db.execute(select(SmartPlug).where(SmartPlug.id == plug_id))
  55. plug = result.scalar_one_or_none()
  56. if not plug:
  57. raise HTTPException(404, "Smart plug not found")
  58. return plug
  59. @router.patch("/{plug_id}", response_model=SmartPlugResponse)
  60. async def update_smart_plug(
  61. plug_id: int,
  62. data: SmartPlugUpdate,
  63. db: AsyncSession = Depends(get_db),
  64. ):
  65. """Update a smart plug."""
  66. result = await db.execute(select(SmartPlug).where(SmartPlug.id == plug_id))
  67. plug = result.scalar_one_or_none()
  68. if not plug:
  69. raise HTTPException(404, "Smart plug not found")
  70. update_data = data.model_dump(exclude_unset=True)
  71. # Validate new printer_id if being changed
  72. if "printer_id" in update_data and update_data["printer_id"]:
  73. new_printer_id = update_data["printer_id"]
  74. # Check printer exists
  75. result = await db.execute(
  76. select(Printer).where(Printer.id == new_printer_id)
  77. )
  78. if not result.scalar_one_or_none():
  79. raise HTTPException(400, "Printer not found")
  80. # Check if that printer already has a different plug assigned
  81. result = await db.execute(
  82. select(SmartPlug).where(
  83. SmartPlug.printer_id == new_printer_id,
  84. SmartPlug.id != plug_id,
  85. )
  86. )
  87. if result.scalar_one_or_none():
  88. raise HTTPException(400, "This printer already has a smart plug assigned")
  89. for field, value in update_data.items():
  90. setattr(plug, field, value)
  91. await db.commit()
  92. await db.refresh(plug)
  93. logger.info(f"Updated smart plug '{plug.name}'")
  94. return plug
  95. @router.delete("/{plug_id}")
  96. async def delete_smart_plug(plug_id: int, db: AsyncSession = Depends(get_db)):
  97. """Delete a smart plug."""
  98. result = await db.execute(select(SmartPlug).where(SmartPlug.id == plug_id))
  99. plug = result.scalar_one_or_none()
  100. if not plug:
  101. raise HTTPException(404, "Smart plug not found")
  102. plug_name = plug.name
  103. await db.delete(plug)
  104. await db.commit()
  105. logger.info(f"Deleted smart plug '{plug_name}'")
  106. return {"message": "Smart plug deleted"}
  107. @router.post("/{plug_id}/control")
  108. async def control_smart_plug(
  109. plug_id: int,
  110. control: SmartPlugControl,
  111. db: AsyncSession = Depends(get_db),
  112. ):
  113. """Manual control: on/off/toggle."""
  114. result = await db.execute(select(SmartPlug).where(SmartPlug.id == plug_id))
  115. plug = result.scalar_one_or_none()
  116. if not plug:
  117. raise HTTPException(404, "Smart plug not found")
  118. if control.action == "on":
  119. success = await tasmota_service.turn_on(plug)
  120. expected_state = "ON"
  121. elif control.action == "off":
  122. success = await tasmota_service.turn_off(plug)
  123. expected_state = "OFF"
  124. elif control.action == "toggle":
  125. success = await tasmota_service.toggle(plug)
  126. expected_state = None # Unknown after toggle
  127. else:
  128. raise HTTPException(400, f"Invalid action: {control.action}")
  129. if not success:
  130. raise HTTPException(503, "Failed to communicate with device")
  131. # Update last state
  132. if expected_state:
  133. plug.last_state = expected_state
  134. plug.last_checked = datetime.utcnow()
  135. await db.commit()
  136. return {"success": True, "action": control.action}
  137. @router.get("/{plug_id}/status", response_model=SmartPlugStatus)
  138. async def get_plug_status(plug_id: int, db: AsyncSession = Depends(get_db)):
  139. """Get current plug status from device."""
  140. result = await db.execute(select(SmartPlug).where(SmartPlug.id == plug_id))
  141. plug = result.scalar_one_or_none()
  142. if not plug:
  143. raise HTTPException(404, "Smart plug not found")
  144. status = await tasmota_service.get_status(plug)
  145. # Update last state in database
  146. if status["reachable"]:
  147. plug.last_state = status["state"]
  148. plug.last_checked = datetime.utcnow()
  149. await db.commit()
  150. return SmartPlugStatus(
  151. state=status["state"],
  152. reachable=status["reachable"],
  153. device_name=status.get("device_name"),
  154. )
  155. @router.post("/test-connection")
  156. async def test_connection(data: SmartPlugTestConnection):
  157. """Test connection to a Tasmota device."""
  158. result = await tasmota_service.test_connection(
  159. data.ip_address,
  160. data.username,
  161. data.password,
  162. )
  163. if not result["success"]:
  164. raise HTTPException(503, result.get("error", "Failed to connect to device"))
  165. return {
  166. "success": True,
  167. "state": result["state"],
  168. "device_name": result.get("device_name"),
  169. }