選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。
 
 
 
 

563 行
16 KiB

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