device_service.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. """
  2. Device management service.
  3. """
  4. from datetime import datetime, timezone
  5. from sqlalchemy import func, select
  6. from sqlalchemy.ext.asyncio import AsyncSession
  7. from app.models.device import Device
  8. from app.schemas.device import DeviceCreate, DeviceUpdate
  9. async def create_device(
  10. db: AsyncSession,
  11. data: DeviceCreate,
  12. ) -> Device:
  13. """
  14. Create a new device.
  15. Args:
  16. db: Database session
  17. data: Device creation data
  18. Returns:
  19. Created device
  20. """
  21. # Check if MAC address already exists
  22. result = await db.execute(
  23. select(Device).where(Device.mac_address == data.mac_address)
  24. )
  25. existing_device = result.scalar_one_or_none()
  26. if existing_device:
  27. raise ValueError(f"Device with MAC {data.mac_address} already exists")
  28. device = Device(
  29. mac_address=data.mac_address,
  30. organization_id=data.organization_id,
  31. status="offline",
  32. config=data.config or {},
  33. # simple_id will be auto-generated by PostgreSQL sequence
  34. )
  35. db.add(device)
  36. await db.commit()
  37. await db.refresh(device)
  38. return device
  39. async def get_device(db: AsyncSession, device_id: int) -> Device | None:
  40. """
  41. Get device by ID.
  42. Args:
  43. db: Database session
  44. device_id: Device ID
  45. Returns:
  46. Device or None
  47. """
  48. result = await db.execute(select(Device).where(Device.id == device_id))
  49. return result.scalar_one_or_none()
  50. async def get_device_by_mac(db: AsyncSession, mac_address: str) -> Device | None:
  51. """
  52. Get device by MAC address.
  53. Args:
  54. db: Database session
  55. mac_address: Device MAC address
  56. Returns:
  57. Device or None
  58. """
  59. result = await db.execute(
  60. select(Device).where(Device.mac_address == mac_address)
  61. )
  62. return result.scalar_one_or_none()
  63. async def list_devices(
  64. db: AsyncSession,
  65. skip: int = 0,
  66. limit: int = 100,
  67. organization_id: int | None = None,
  68. status: str | None = None,
  69. ) -> tuple[list[Device], int]:
  70. """
  71. List devices with pagination and filters.
  72. Args:
  73. db: Database session
  74. skip: Number of records to skip
  75. limit: Maximum number of records to return
  76. organization_id: Filter by organization (optional)
  77. status: Filter by status (optional)
  78. Returns:
  79. Tuple of (devices list, total count)
  80. """
  81. # Build query
  82. query = select(Device)
  83. if organization_id is not None:
  84. query = query.where(Device.organization_id == organization_id)
  85. if status:
  86. query = query.where(Device.status == status)
  87. # Get total count
  88. count_query = select(func.count()).select_from(Device)
  89. if organization_id is not None:
  90. count_query = count_query.where(Device.organization_id == organization_id)
  91. if status:
  92. count_query = count_query.where(Device.status == status)
  93. total_result = await db.execute(count_query)
  94. total = total_result.scalar_one()
  95. # Get paginated results
  96. query = query.offset(skip).limit(limit).order_by(Device.simple_id)
  97. result = await db.execute(query)
  98. devices = list(result.scalars().all())
  99. return devices, total
  100. async def update_device(
  101. db: AsyncSession,
  102. device_id: int,
  103. data: DeviceUpdate,
  104. ) -> Device | None:
  105. """
  106. Update device.
  107. Args:
  108. db: Database session
  109. device_id: Device ID
  110. data: Update data
  111. Returns:
  112. Updated device or None if not found
  113. """
  114. result = await db.execute(select(Device).where(Device.id == device_id))
  115. device = result.scalar_one_or_none()
  116. if not device:
  117. return None
  118. # Update fields
  119. update_data = data.model_dump(exclude_unset=True)
  120. for field, value in update_data.items():
  121. setattr(device, field, value)
  122. await db.commit()
  123. await db.refresh(device)
  124. return device
  125. async def delete_device(
  126. db: AsyncSession,
  127. device_id: int,
  128. ) -> bool:
  129. """
  130. Delete device.
  131. Args:
  132. db: Database session
  133. device_id: Device ID
  134. Returns:
  135. True if deleted, False if not found
  136. """
  137. result = await db.execute(select(Device).where(Device.id == device_id))
  138. device = result.scalar_one_or_none()
  139. if not device:
  140. return False
  141. await db.delete(device)
  142. await db.commit()
  143. return True
  144. async def update_device_heartbeat(
  145. db: AsyncSession,
  146. mac_address: str,
  147. ) -> Device | None:
  148. """
  149. Update device last_seen_at timestamp (heartbeat).
  150. Args:
  151. db: Database session
  152. mac_address: Device MAC address
  153. Returns:
  154. Updated device or None if not found
  155. """
  156. result = await db.execute(
  157. select(Device).where(Device.mac_address == mac_address)
  158. )
  159. device = result.scalar_one_or_none()
  160. if not device:
  161. return None
  162. device.last_seen_at = datetime.now(timezone.utc)
  163. device.status = "online"
  164. await db.commit()
  165. await db.refresh(device)
  166. return device