device_service.py 6.0 KB

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