Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 
 

559 строки
17 KiB

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