settings.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. import json
  2. from datetime import datetime
  3. from fastapi import APIRouter, Depends, UploadFile, File
  4. from fastapi.responses import JSONResponse
  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.settings import Settings
  9. from backend.app.models.notification import NotificationProvider
  10. from backend.app.models.smart_plug import SmartPlug
  11. from backend.app.schemas.settings import AppSettings, AppSettingsUpdate
  12. router = APIRouter(prefix="/settings", tags=["settings"])
  13. # Default settings
  14. DEFAULT_SETTINGS = AppSettings()
  15. async def get_setting(db: AsyncSession, key: str) -> str | None:
  16. """Get a single setting value by key."""
  17. result = await db.execute(select(Settings).where(Settings.key == key))
  18. setting = result.scalar_one_or_none()
  19. return setting.value if setting else None
  20. async def set_setting(db: AsyncSession, key: str, value: str) -> None:
  21. """Set a single setting value."""
  22. result = await db.execute(select(Settings).where(Settings.key == key))
  23. setting = result.scalar_one_or_none()
  24. if setting:
  25. setting.value = value
  26. else:
  27. setting = Settings(key=key, value=value)
  28. db.add(setting)
  29. @router.get("/", response_model=AppSettings)
  30. async def get_settings(db: AsyncSession = Depends(get_db)):
  31. """Get all application settings."""
  32. settings_dict = DEFAULT_SETTINGS.model_dump()
  33. # Load saved settings from database
  34. result = await db.execute(select(Settings))
  35. db_settings = result.scalars().all()
  36. for setting in db_settings:
  37. if setting.key in settings_dict:
  38. # Parse the value based on the expected type
  39. if setting.key in ["auto_archive", "save_thumbnails", "capture_finish_photo", "spoolman_enabled", "check_updates"]:
  40. settings_dict[setting.key] = setting.value.lower() == "true"
  41. elif setting.key in ["default_filament_cost", "energy_cost_per_kwh", "ams_temp_good", "ams_temp_fair"]:
  42. settings_dict[setting.key] = float(setting.value)
  43. elif setting.key in ["ams_humidity_good", "ams_humidity_fair"]:
  44. settings_dict[setting.key] = int(setting.value)
  45. elif setting.key == "default_printer_id":
  46. # Handle nullable integer
  47. settings_dict[setting.key] = int(setting.value) if setting.value and setting.value != "None" else None
  48. else:
  49. settings_dict[setting.key] = setting.value
  50. return AppSettings(**settings_dict)
  51. @router.put("/", response_model=AppSettings)
  52. async def update_settings(
  53. settings_update: AppSettingsUpdate,
  54. db: AsyncSession = Depends(get_db),
  55. ):
  56. """Update application settings."""
  57. update_data = settings_update.model_dump(exclude_unset=True)
  58. for key, value in update_data.items():
  59. # Convert value to string for storage
  60. if isinstance(value, bool):
  61. str_value = "true" if value else "false"
  62. elif value is None:
  63. str_value = "None"
  64. else:
  65. str_value = str(value)
  66. await set_setting(db, key, str_value)
  67. await db.commit()
  68. # Return updated settings
  69. return await get_settings(db)
  70. @router.post("/reset", response_model=AppSettings)
  71. async def reset_settings(db: AsyncSession = Depends(get_db)):
  72. """Reset all settings to defaults."""
  73. # Delete all settings
  74. result = await db.execute(select(Settings))
  75. for setting in result.scalars().all():
  76. await db.delete(setting)
  77. await db.commit()
  78. return DEFAULT_SETTINGS
  79. @router.get("/check-ffmpeg")
  80. async def check_ffmpeg():
  81. """Check if ffmpeg is installed and available."""
  82. from backend.app.services.camera import get_ffmpeg_path
  83. ffmpeg_path = get_ffmpeg_path()
  84. return {
  85. "installed": ffmpeg_path is not None,
  86. "path": ffmpeg_path,
  87. }
  88. @router.get("/spoolman")
  89. async def get_spoolman_settings(db: AsyncSession = Depends(get_db)):
  90. """Get Spoolman integration settings."""
  91. spoolman_enabled = await get_setting(db, "spoolman_enabled") or "false"
  92. spoolman_url = await get_setting(db, "spoolman_url") or ""
  93. spoolman_sync_mode = await get_setting(db, "spoolman_sync_mode") or "auto"
  94. return {
  95. "spoolman_enabled": spoolman_enabled,
  96. "spoolman_url": spoolman_url,
  97. "spoolman_sync_mode": spoolman_sync_mode,
  98. }
  99. @router.put("/spoolman")
  100. async def update_spoolman_settings(
  101. settings: dict,
  102. db: AsyncSession = Depends(get_db),
  103. ):
  104. """Update Spoolman integration settings."""
  105. if "spoolman_enabled" in settings:
  106. await set_setting(db, "spoolman_enabled", settings["spoolman_enabled"])
  107. if "spoolman_url" in settings:
  108. await set_setting(db, "spoolman_url", settings["spoolman_url"])
  109. if "spoolman_sync_mode" in settings:
  110. await set_setting(db, "spoolman_sync_mode", settings["spoolman_sync_mode"])
  111. await db.commit()
  112. # Return updated settings
  113. return await get_spoolman_settings(db)
  114. @router.get("/backup")
  115. async def export_backup(db: AsyncSession = Depends(get_db)):
  116. """Export all settings, notification providers, and smart plugs as JSON backup."""
  117. # Get all settings
  118. result = await db.execute(select(Settings))
  119. db_settings = result.scalars().all()
  120. settings_data = {s.key: s.value for s in db_settings}
  121. # Get notification providers
  122. result = await db.execute(select(NotificationProvider))
  123. providers = result.scalars().all()
  124. providers_data = []
  125. for p in providers:
  126. providers_data.append({
  127. "name": p.name,
  128. "provider_type": p.provider_type,
  129. "enabled": p.enabled,
  130. "config": json.loads(p.config) if isinstance(p.config, str) else p.config,
  131. "on_print_start": p.on_print_start,
  132. "on_print_complete": p.on_print_complete,
  133. "on_print_failed": p.on_print_failed,
  134. "on_print_stopped": p.on_print_stopped,
  135. "on_print_progress": p.on_print_progress,
  136. "on_printer_offline": p.on_printer_offline,
  137. "on_printer_error": p.on_printer_error,
  138. "on_filament_low": p.on_filament_low,
  139. "on_maintenance_due": p.on_maintenance_due,
  140. "quiet_hours_enabled": p.quiet_hours_enabled,
  141. "quiet_hours_start": p.quiet_hours_start,
  142. "quiet_hours_end": p.quiet_hours_end,
  143. })
  144. # Get smart plugs
  145. result = await db.execute(select(SmartPlug))
  146. plugs = result.scalars().all()
  147. plugs_data = []
  148. for plug in plugs:
  149. plugs_data.append({
  150. "name": plug.name,
  151. "ip_address": plug.ip_address,
  152. "enabled": plug.enabled,
  153. "auto_off_enabled": plug.auto_off_enabled,
  154. "auto_off_delay_minutes": plug.auto_off_delay_minutes,
  155. })
  156. backup = {
  157. "version": "1.0",
  158. "exported_at": datetime.utcnow().isoformat(),
  159. "settings": settings_data,
  160. "notification_providers": providers_data,
  161. "smart_plugs": plugs_data,
  162. }
  163. return JSONResponse(
  164. content=backup,
  165. headers={
  166. "Content-Disposition": f"attachment; filename=bambutrack-backup-{datetime.now().strftime('%Y%m%d-%H%M%S')}.json"
  167. }
  168. )
  169. @router.post("/restore")
  170. async def import_backup(
  171. file: UploadFile = File(...),
  172. db: AsyncSession = Depends(get_db),
  173. ):
  174. """Restore settings, notification providers, and smart plugs from JSON backup."""
  175. try:
  176. content = await file.read()
  177. backup = json.loads(content.decode("utf-8"))
  178. except Exception as e:
  179. return {"success": False, "message": f"Invalid backup file: {str(e)}"}
  180. restored = {"settings": 0, "notification_providers": 0, "smart_plugs": 0}
  181. # Restore settings
  182. if "settings" in backup:
  183. for key, value in backup["settings"].items():
  184. await set_setting(db, key, value)
  185. restored["settings"] += 1
  186. # Restore notification providers (skip duplicates by name)
  187. if "notification_providers" in backup:
  188. for provider_data in backup["notification_providers"]:
  189. # Check if provider with same name exists
  190. result = await db.execute(
  191. select(NotificationProvider).where(NotificationProvider.name == provider_data["name"])
  192. )
  193. existing = result.scalar_one_or_none()
  194. if not existing:
  195. provider = NotificationProvider(
  196. name=provider_data["name"],
  197. provider_type=provider_data["provider_type"],
  198. enabled=provider_data.get("enabled", True),
  199. config=json.dumps(provider_data.get("config", {})),
  200. on_print_start=provider_data.get("on_print_start", False),
  201. on_print_complete=provider_data.get("on_print_complete", True),
  202. on_print_failed=provider_data.get("on_print_failed", True),
  203. on_print_stopped=provider_data.get("on_print_stopped", True),
  204. on_print_progress=provider_data.get("on_print_progress", False),
  205. on_printer_offline=provider_data.get("on_printer_offline", False),
  206. on_printer_error=provider_data.get("on_printer_error", False),
  207. on_filament_low=provider_data.get("on_filament_low", False),
  208. on_maintenance_due=provider_data.get("on_maintenance_due", False),
  209. quiet_hours_enabled=provider_data.get("quiet_hours_enabled", False),
  210. quiet_hours_start=provider_data.get("quiet_hours_start"),
  211. quiet_hours_end=provider_data.get("quiet_hours_end"),
  212. )
  213. db.add(provider)
  214. restored["notification_providers"] += 1
  215. # Restore smart plugs (skip duplicates by IP)
  216. if "smart_plugs" in backup:
  217. for plug_data in backup["smart_plugs"]:
  218. # Check if plug with same IP exists
  219. result = await db.execute(
  220. select(SmartPlug).where(SmartPlug.ip_address == plug_data["ip_address"])
  221. )
  222. existing = result.scalar_one_or_none()
  223. if not existing:
  224. plug = SmartPlug(
  225. name=plug_data["name"],
  226. ip_address=plug_data["ip_address"],
  227. enabled=plug_data.get("enabled", True),
  228. auto_off_enabled=plug_data.get("auto_off_enabled", False),
  229. auto_off_delay_minutes=plug_data.get("auto_off_delay_minutes", 5),
  230. )
  231. db.add(plug)
  232. restored["smart_plugs"] += 1
  233. await db.commit()
  234. return {
  235. "success": True,
  236. "message": f"Restored {restored['settings']} settings, {restored['notification_providers']} notification providers, {restored['smart_plugs']} smart plugs",
  237. "restored": restored,
  238. }