Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 
 
 

564 lignes
17 KiB

  1. import asyncio
  2. import json
  3. from fastapi import APIRouter, Depends, HTTPException, Query, Request
  4. import httpx
  5. import config_env
  6. from typing import List, Optional
  7. from urllib.parse import urlencode
  8. from schemas.reslevis import (
  9. BuildingItem,
  10. FloorItem,
  11. ZoneItem,
  12. GatewayItem,
  13. TrackerItem,
  14. OperatorItem,
  15. SubjectItem,
  16. AlarmItem,
  17. TrackItem,
  18. TrackHistoryItem,
  19. TrackerZoneItem,
  20. SettingItem,
  21. )
  22. from logica_reslevis.gateway import GatewayJsonRepository
  23. from logica_reslevis.building import BuildingJsonRepository
  24. from logica_reslevis.floor import FloorJsonRepository
  25. from logica_reslevis.zone import ZoneJsonRepository
  26. from logica_reslevis.tracker import TrackerJsonRepository
  27. from logica_reslevis.operator import OperatorJsonRepository
  28. from logica_reslevis.setting import SettingJsonRepository
  29. from logica_reslevis.subject import SubjectJsonRepository
  30. from logica_reslevis.alarm import AlarmJsonRepository
  31. from logica_reslevis.track import TrackJsonRepository
  32. from logica_reslevis.tracker_zone import TrackerZoneJsonRepository
  33. from security import get_current_user
  34. #CORE SYNC
  35. CORE_BASE_URL = config_env.CORE_API_URL.rstrip("/")
  36. CORE_TIMEOUT = 2.0 # secondi
  37. async def sync_core_get(request: Request) -> None:
  38. if request.method != "GET":
  39. return
  40. sync = CORE_GET_SYNC.get(request.url.path)
  41. if sync is None:
  42. return
  43. repo, normalizer = sync
  44. try:
  45. async with httpx.AsyncClient(timeout=CORE_TIMEOUT) as client:
  46. resp = await client.get(
  47. f"{CORE_BASE_URL}{request.url.path}",
  48. params=request.query_params,
  49. )
  50. if 200 <= resp.status_code < 300:
  51. data = resp.json()
  52. if isinstance(data, list):
  53. if normalizer:
  54. data = [normalizer(r) for r in data if isinstance(r, dict)]
  55. repo._write_all(data) # aggiorna i file locali
  56. except (httpx.RequestError, ValueError):
  57. # CORE giù o risposta non valida -> uso il file locale
  58. pass
  59. router = APIRouter(dependencies=[Depends(sync_core_get)])
  60. gateway_repo = GatewayJsonRepository()
  61. building_repo = BuildingJsonRepository()
  62. floor_repo = FloorJsonRepository()
  63. zone_repo = ZoneJsonRepository()
  64. tracker_repo = TrackerJsonRepository()
  65. operator_repo = OperatorJsonRepository()
  66. subject_repo = SubjectJsonRepository()
  67. alarm_repo = AlarmJsonRepository()
  68. track_repo = TrackJsonRepository()
  69. tracker_zone_repo = TrackerZoneJsonRepository()
  70. setting_repo = SettingJsonRepository()
  71. def _none_if_empty(v):
  72. return None if v in ("", None, 0, "0") else v
  73. def _str_or_none(v):
  74. if v in ("", None):
  75. return None
  76. if isinstance(v, (int, float, bool)):
  77. return str(v)
  78. return v
  79. def _uuid_list(values):
  80. if values in ("", None):
  81. return []
  82. if isinstance(values, str):
  83. values = [v for v in values.split(",") if v]
  84. if isinstance(values, (list, tuple, set)):
  85. cleaned = []
  86. for v in values:
  87. if isinstance(v, dict):
  88. v = v.get("id") or v.get("uuid")
  89. if v in ("", None, 0, "0"):
  90. continue
  91. cleaned.append(v)
  92. return cleaned
  93. return [values] if values not in ("", None, 0, "0") else []
  94. def _normalize_gateway(row: dict) -> dict:
  95. row = dict(row)
  96. row["floor"] = _none_if_empty(row.get("floor"))
  97. row["building"] = _none_if_empty(row.get("building"))
  98. return row
  99. def _normalize_tracker(row: dict) -> dict:
  100. row = dict(row)
  101. row["floor"] = _none_if_empty(row.get("floor"))
  102. row["building"] = _none_if_empty(row.get("building"))
  103. row["battery"] = _str_or_none(row.get("battery"))
  104. row["temperature"] = _str_or_none(row.get("temperature"))
  105. row["acceleration"] = _str_or_none(row.get("acceleration"))
  106. row["heartRate"] = _str_or_none(row.get("heartRate"))
  107. return row
  108. def _normalize_track(row: dict) -> dict:
  109. row = dict(row)
  110. row["ID"] = row.get("ID")
  111. row["gateway"] = _none_if_empty(row.get("gateway"))
  112. row["tracker"] = _none_if_empty(row.get("tracker"))
  113. row["subject"] = _none_if_empty(row.get("subject"))
  114. row["floor"] = _none_if_empty(row.get("floor"))
  115. row["building"] = _none_if_empty(row.get("building"))
  116. row["timestamp"] = _str_or_none(row.get("timestamp"))
  117. row["type"] = _str_or_none(row.get("type"))
  118. row["status"] = _str_or_none(row.get("status"))
  119. row["gatewayMac"] = _str_or_none(row.get("gatewayMac"))
  120. row["trackerMac"] = _str_or_none(row.get("trackerMac"))
  121. row["subjectName"] = _str_or_none(row.get("subjectName"))
  122. row["x"] = None if row.get("x") in ("", None) else row.get("x")
  123. row["y"] = None if row.get("y") in ("", None) else row.get("y")
  124. row["z"] = None if row.get("z") in ("", None) else row.get("z")
  125. # signal resta float o None
  126. row["signal"] = None if row.get("signal") in ("", None) else row.get("signal")
  127. return row
  128. def _normalize_zone(row: dict) -> dict:
  129. row = dict(row)
  130. row["floor"] = _none_if_empty(row.get("floor"))
  131. row["building"] = _none_if_empty(row.get("building"))
  132. row["groups"] = _uuid_list(row.get("groups"))
  133. return row
  134. CORE_GET_SYNC = {
  135. "/reslevis/getGateways": (gateway_repo, _normalize_gateway),
  136. "/reslevis/getZones": (zone_repo, _normalize_zone),
  137. "/reslevis/getTrackers": (tracker_repo, _normalize_tracker),
  138. }
  139. async def _core_get_json(
  140. client: httpx.AsyncClient,
  141. path: str,
  142. params: Optional[dict] = None,
  143. ):
  144. resp = await client.get(f"{CORE_BASE_URL}{path}", params=params)
  145. if resp.status_code >= 400:
  146. detail = resp.text.strip() or "CORE request failed"
  147. raise HTTPException(status_code=resp.status_code, detail=detail)
  148. try:
  149. return resp.json()
  150. except ValueError as exc:
  151. raise HTTPException(status_code=502, detail="Invalid CORE response") from exc
  152. async def _core_get_json_via_curl(
  153. path: str,
  154. params: Optional[dict] = None,
  155. ):
  156. query_string = urlencode(params or {})
  157. url = f"{CORE_BASE_URL}{path}"
  158. if query_string:
  159. url = f"{url}?{query_string}"
  160. cmd = ["curl", "-sS", "-X", "GET"]
  161. if CORE_BASE_URL.startswith("https://"):
  162. cmd.append("-k")
  163. cmd.append(url)
  164. process = await asyncio.create_subprocess_exec(
  165. *cmd,
  166. stdout=asyncio.subprocess.PIPE,
  167. stderr=asyncio.subprocess.PIPE,
  168. )
  169. stdout, stderr = await process.communicate()
  170. if process.returncode != 0:
  171. detail = (stderr or stdout).decode("utf-8", errors="replace").strip() or "CORE curl request failed"
  172. raise HTTPException(status_code=502, detail=detail)
  173. try:
  174. return json.loads(stdout.decode("utf-8"))
  175. except ValueError as exc:
  176. raise HTTPException(status_code=502, detail="Invalid CORE response") from exc
  177. async def _fetch_tracks_for_tracker(
  178. tracker_id: str,
  179. params: Optional[dict] = None,
  180. ) -> List[dict]:
  181. payload = await _core_get_json_via_curl(f"/reslevis/getTracks/{tracker_id}", params=params)
  182. if not isinstance(payload, list):
  183. raise HTTPException(status_code=502, detail="Unexpected CORE response type")
  184. return [_normalize_track(row) for row in payload if isinstance(row, dict)]
  185. def _sort_tracks_desc(rows: List[dict]) -> List[dict]:
  186. return sorted(rows, key=lambda row: row.get("timestamp") or "", reverse=True)
  187. async def _fetch_all_tracks(params: dict) -> List[dict]:
  188. trackers_payload = await _core_get_json_via_curl("/reslevis/getTrackers")
  189. if not isinstance(trackers_payload, list):
  190. raise HTTPException(status_code=502, detail="Unexpected CORE tracker response type")
  191. tracker_ids = []
  192. for item in trackers_payload:
  193. if not isinstance(item, dict):
  194. continue
  195. tracker_id = item.get("id")
  196. if tracker_id:
  197. tracker_ids.append(str(tracker_id))
  198. batches = await asyncio.gather(
  199. *[_fetch_tracks_for_tracker(tracker_id, params) for tracker_id in tracker_ids]
  200. )
  201. merged = [row for batch in batches for row in batch]
  202. merged = _sort_tracks_desc(merged)
  203. limit = params.get("limit")
  204. if isinstance(limit, int):
  205. return merged[:limit]
  206. return merged
  207. @router.get(
  208. "/getGateways",
  209. response_model=List[GatewayItem],
  210. tags=["Reslevis"],
  211. dependencies=[Depends(get_current_user)],
  212. )
  213. def getGateways():
  214. return gateway_repo.list()
  215. @router.post("/postGateway", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  216. def postGateway(item: GatewayItem):
  217. gateway_repo.add(item)
  218. return {"message": "OK"}
  219. @router.put("/updateGateway", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  220. def updateGateway(item: GatewayItem):
  221. gateway_repo.update(item)
  222. return {"message": "OK"}
  223. @router.delete("/removeGateway/{gateway_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  224. def removeGateway(gateway_id: str):
  225. gateway_repo.remove(gateway_id)
  226. return {"message": "OK"}
  227. @router.get(
  228. "/getBuildings",
  229. response_model=List[BuildingItem],
  230. tags=["Reslevis"],
  231. dependencies=[Depends(get_current_user)],
  232. )
  233. def getBuildings():
  234. return building_repo.list()
  235. @router.post("/postBuilding", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  236. def postBuilding(item: BuildingItem):
  237. building_repo.add(item)
  238. return {"message": "OK"}
  239. @router.put("/updateBuilding", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  240. def updateBuilding(item: BuildingItem):
  241. building_repo.update(item)
  242. return {"message": "OK"}
  243. @router.delete("/removeBuilding/{building_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  244. def removeBuilding(building_id: str):
  245. building_repo.remove(building_id)
  246. return {"message": "OK"}
  247. @router.get(
  248. "/getFloors",
  249. response_model=List[FloorItem],
  250. tags=["Reslevis"],
  251. dependencies=[Depends(get_current_user)],
  252. )
  253. def getFloors():
  254. return floor_repo.list()
  255. @router.post("/postFloor", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  256. def postFloor(item: FloorItem):
  257. floor_repo.add(item)
  258. return {"message": "OK"}
  259. @router.put("/updateFloor", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  260. def updateFloor(item: FloorItem):
  261. floor_repo.update(item)
  262. return {"message": "OK"}
  263. @router.delete("/removeFloor/{floor_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  264. def removeFloor(floor_id: str):
  265. floor_repo.remove(floor_id)
  266. return {"message": "OK"}
  267. @router.get(
  268. "/getZones",
  269. response_model=List[ZoneItem],
  270. tags=["Reslevis"],
  271. dependencies=[Depends(get_current_user)],
  272. )
  273. def getZones():
  274. return zone_repo.list()
  275. @router.post("/postZone", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  276. def postZone(item: ZoneItem):
  277. zone_repo.add(item)
  278. return {"message": "OK"}
  279. @router.put("/updateZone", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  280. def updateZone(item: ZoneItem):
  281. zone_repo.update(item)
  282. return {"message": "OK"}
  283. @router.delete("/removeZone/{zone_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  284. def removeZone(zone_id: str):
  285. zone_repo.remove(zone_id)
  286. return {"message": "OK"}
  287. @router.get(
  288. "/getTrackers",
  289. response_model=List[TrackerItem],
  290. tags=["Reslevis"],
  291. dependencies=[Depends(get_current_user)],
  292. )
  293. def getTrackers():
  294. return tracker_repo.list()
  295. @router.post("/postTracker", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  296. def postTracker(item: TrackerItem):
  297. tracker_repo.add(item)
  298. return {"message": "OK"}
  299. @router.put("/updateTracker", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  300. def updateTracker(item: TrackerItem):
  301. tracker_repo.update(item)
  302. return {"message": "OK"}
  303. @router.delete("/removeTracker/{tracker_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  304. def removeTracker(tracker_id: str):
  305. tracker_repo.remove(tracker_id)
  306. return {"message": "OK"}
  307. @router.get(
  308. "/getTrackerZones",
  309. response_model=List[TrackerZoneItem],
  310. tags=["Reslevis"],
  311. dependencies=[Depends(get_current_user)],
  312. )
  313. def getTrackerZones():
  314. return tracker_zone_repo.list()
  315. @router.post("/postTrackerZone", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  316. def postTrackerZone(item: TrackerZoneItem):
  317. tracker_zone_repo.add(item)
  318. return {"message": "OK"}
  319. @router.put("/updateTrackerZone", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  320. def updateTrackerZone(item: TrackerZoneItem):
  321. tracker_zone_repo.update(item)
  322. return {"message": "OK"}
  323. @router.delete("/removeTrackerZone/{tracker_zone_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  324. def removeTrackerZone(tracker_zone_id: str):
  325. tracker_zone_repo.remove(tracker_zone_id)
  326. return {"message": "OK"}
  327. @router.get(
  328. "/getTracks",
  329. response_model=List[TrackHistoryItem],
  330. tags=["Reslevis"],
  331. dependencies=[Depends(get_current_user)],
  332. )
  333. async def getTracks(
  334. request: Request,
  335. tracker_id: Optional[str] = Query(None),
  336. limit: Optional[int] = Query(None, ge=1),
  337. from_: Optional[str] = Query(None, alias="from"),
  338. to: Optional[str] = Query(None),
  339. ):
  340. params = {}
  341. if limit is not None:
  342. params["limit"] = limit
  343. if from_:
  344. params["from"] = from_
  345. if to:
  346. params["to"] = to
  347. selected_tracker_id = tracker_id or request.query_params.get("id")
  348. if selected_tracker_id:
  349. return await _fetch_tracks_for_tracker(selected_tracker_id, params)
  350. return await _fetch_all_tracks(params)
  351. @router.get(
  352. "/getTracks/{tracker_id}",
  353. response_model=List[TrackHistoryItem],
  354. tags=["Reslevis"],
  355. dependencies=[Depends(get_current_user)],
  356. )
  357. async def getTrack(
  358. tracker_id: str,
  359. limit: Optional[int] = Query(None, ge=1),
  360. from_: Optional[str] = Query(None, alias="from"),
  361. to: Optional[str] = Query(None),
  362. ):
  363. params = {}
  364. if limit is not None:
  365. params["limit"] = limit
  366. if from_:
  367. params["from"] = from_
  368. if to:
  369. params["to"] = to
  370. return await _fetch_tracks_for_tracker(tracker_id, params)
  371. @router.post("/postTrack", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  372. def postTrack(item: TrackItem):
  373. track_repo.add(item)
  374. return {"message": "OK"}
  375. @router.put("/updateTrack", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  376. def updateTrack(item: TrackItem):
  377. track_repo.update(item)
  378. return {"message": "OK"}
  379. @router.delete("/removeTrack/{track_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  380. def removeTrack(track_id: str):
  381. track_repo.remove(track_id)
  382. return {"message": "OK"}
  383. @router.get(
  384. "/getOperators",
  385. response_model=List[OperatorItem],
  386. tags=["Reslevis"],
  387. dependencies=[Depends(get_current_user)],
  388. )
  389. def getOperators():
  390. return operator_repo.list()
  391. @router.post("/postOperator", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  392. def postOperator(item: OperatorItem):
  393. operator_repo.add(item)
  394. return {"message": "OK"}
  395. @router.put("/updateOperator", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  396. def updateOperator(item: OperatorItem):
  397. operator_repo.update(item)
  398. return {"message": "OK"}
  399. @router.delete("/removeOperator/{operator_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  400. def removeOperator(operator_id: str):
  401. operator_repo.remove(operator_id)
  402. return {"message": "OK"}
  403. @router.get(
  404. "/getSubjects",
  405. response_model=List[SubjectItem],
  406. tags=["Reslevis"],
  407. dependencies=[Depends(get_current_user)],
  408. )
  409. def getSubjects():
  410. return subject_repo.list()
  411. @router.post("/postSubject", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  412. def postSubject(item: SubjectItem):
  413. subject_repo.add(item)
  414. return {"message": "OK"}
  415. @router.put("/updateSubject", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  416. def updateSubject(item: SubjectItem):
  417. subject_repo.update(item)
  418. return {"message": "OK"}
  419. @router.delete("/removeSubject/{subject_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  420. def removeSubject(subject_id: str):
  421. subject_repo.remove(subject_id)
  422. return {"message": "OK"}
  423. @router.get(
  424. "/getSettings",
  425. response_model=List[SettingItem],
  426. tags=["Reslevis"],
  427. dependencies=[Depends(get_current_user)],
  428. )
  429. def getSettings():
  430. return setting_repo.list()
  431. @router.post("/postSetting", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  432. def postSetting(item: SettingItem):
  433. setting_repo.add(item)
  434. return {"message": "OK"}
  435. @router.put("/updateSetting", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  436. def updateSetting(item: SettingItem):
  437. setting_repo.update(item)
  438. return {"message": "OK"}
  439. @router.delete("/removeSetting/{setting_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  440. def removeSetting(setting_id: str):
  441. setting_repo.remove(setting_id)
  442. return {"message": "OK"}