您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
 
 
 
 

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