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

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