import asyncio import json from urllib.parse import urlencode from fastapi import APIRouter, Depends, HTTPException, Query, Request import httpx import config_env from typing import List, Optional from schemas.reslevis import ( BuildingItem, FloorItem, ZoneItem, ZoneAreaDefinitionItem, GatewayItem, TrackerItem, OperatorItem, SubjectItem, AlarmItem, AlarmCoreItem, AlarmStatusUpdateItem, TrackItem, TrackHistoryItem, TrackerZoneItem, SettingItem, GuiConfigItem, CoreSettingsItem, CoreSettingsUpdateItem, ) from logica_reslevis.gateway import GatewayJsonRepository from logica_reslevis.building import BuildingJsonRepository from logica_reslevis.floor import FloorJsonRepository from logica_reslevis.zone import ZoneJsonRepository from logica_reslevis.zone_area_definition import ZoneAreaDefinitionJsonRepository from logica_reslevis.tracker import TrackerJsonRepository from logica_reslevis.operator import OperatorJsonRepository from logica_reslevis.setting import SettingJsonRepository from logica_reslevis.gui_config import GuiConfigJsonRepository from logica_reslevis.subject import SubjectJsonRepository from logica_reslevis.alarm import AlarmJsonRepository from logica_reslevis.track import TrackJsonRepository from logica_reslevis.tracker_zone import TrackerZoneJsonRepository from logica_reslevis.tracker_mode import get_mode_aware_trackers from security import get_current_user #CORE SYNC CORE_BASE_URL = config_env.CORE_API_URL.rstrip("/") ALERTS_CORE_BASE_URL = "http://localhost:1902" TRACKS_CORE_BASE_URL = "http://localhost:1902" <<<<<<< HEAD SETTINGS_CORE_BASE_URL = "http://localhost:1902" ======= SETTINGS_CORE_BASE_URL = "http://127.0.0.1:1902" >>>>>>> Tracker_bug_fix_20260429 CORE_TIMEOUT = 2.0 # secondi async def sync_core_get(request: Request) -> None: if request.method != "GET": return sync = CORE_GET_SYNC.get(request.url.path) if sync is None: return repo, normalizer = sync try: async with httpx.AsyncClient(timeout=CORE_TIMEOUT) as client: resp = await client.get( f"{CORE_BASE_URL}{request.url.path}", params=request.query_params, ) if 200 <= resp.status_code < 300: data = resp.json() if isinstance(data, list): if normalizer: data = [normalizer(r) for r in data if isinstance(r, dict)] repo._write_all(data) # aggiorna i file locali except (httpx.RequestError, ValueError): # CORE giù o risposta non valida -> uso il file locale pass router = APIRouter(dependencies=[Depends(sync_core_get)]) gateway_repo = GatewayJsonRepository() building_repo = BuildingJsonRepository() floor_repo = FloorJsonRepository() zone_repo = ZoneJsonRepository() zone_area_definition_repo = ZoneAreaDefinitionJsonRepository() tracker_repo = TrackerJsonRepository() operator_repo = OperatorJsonRepository() subject_repo = SubjectJsonRepository() alarm_repo = AlarmJsonRepository() track_repo = TrackJsonRepository() tracker_zone_repo = TrackerZoneJsonRepository() setting_repo = SettingJsonRepository() gui_config_repo = GuiConfigJsonRepository() def _none_if_empty(v): return None if v in ("", None, 0, "0") else v def _str_or_none(v): if v in ("", None): return None if isinstance(v, (int, float, bool)): return str(v) return v def _uuid_list(values): if values in ("", None): return [] if isinstance(values, str): values = [v for v in values.split(",") if v] if isinstance(values, (list, tuple, set)): cleaned = [] for v in values: if isinstance(v, dict): v = v.get("id") or v.get("uuid") if v in ("", None, 0, "0"): continue cleaned.append(v) return cleaned return [values] if values not in ("", None, 0, "0") else [] def _normalize_gateway(row: dict) -> dict: row = dict(row) row["floor"] = _none_if_empty(row.get("floor")) row["building"] = _none_if_empty(row.get("building")) return row def _normalize_track(row: dict) -> dict: row = dict(row) row["ID"] = row.get("ID") row["gateway"] = _none_if_empty(row.get("gateway")) row["tracker"] = _none_if_empty(row.get("tracker")) row["subject"] = _none_if_empty(row.get("subject")) row["floor"] = _none_if_empty(row.get("floor")) row["building"] = _none_if_empty(row.get("building")) row["timestamp"] = _str_or_none(row.get("timestamp")) row["type"] = _str_or_none(row.get("type")) row["status"] = _str_or_none(row.get("status")) row["gatewayMac"] = _str_or_none(row.get("gatewayMac")) row["trackerMac"] = _str_or_none(row.get("trackerMac")) row["subjectName"] = _str_or_none(row.get("subjectName")) row["x"] = None if row.get("x") in ("", None) else row.get("x") row["y"] = None if row.get("y") in ("", None) else row.get("y") row["z"] = None if row.get("z") in ("", None) else row.get("z") # signal resta float o None row["signal"] = None if row.get("signal") in ("", None) else row.get("signal") return row def _normalize_zone(row: dict) -> dict: row = dict(row) row["floor"] = _none_if_empty(row.get("floor")) row["building"] = _none_if_empty(row.get("building")) row["groups"] = _uuid_list(row.get("groups")) return row CORE_GET_SYNC = { "/reslevis/getGateways": (gateway_repo, _normalize_gateway), "/reslevis/getZones": (zone_repo, _normalize_zone), } async def _fetch_tracks_for_tracker( tracker_id: str, params: Optional[dict] = None, ) -> List[dict]: query_string = urlencode(params or {}) url = f"{TRACKS_CORE_BASE_URL}/reslevis/getTracks/{tracker_id}" if query_string: url = f"{url}?{query_string}" process = await asyncio.create_subprocess_exec( "curl", "-sS", "-X", "GET", url, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) stdout, stderr = await process.communicate() if process.returncode != 0: detail = (stderr or stdout).decode("utf-8", errors="replace").strip() or "CORE curl request failed" raise HTTPException(status_code=502, detail=detail) try: payload = json.loads(stdout.decode("utf-8")) except ValueError as exc: raise HTTPException(status_code=502, detail="Invalid CORE response") from exc if not isinstance(payload, list): raise HTTPException(status_code=502, detail="Unexpected CORE response type") return [_normalize_track(row) for row in payload if isinstance(row, dict)] @router.get( "/getGateways", response_model=List[GatewayItem], tags=["Reslevis"], dependencies=[Depends(get_current_user)], ) def getGateways(): return gateway_repo.list() @router.post("/postGateway", tags=["Reslevis"], dependencies=[Depends(get_current_user)]) def postGateway(item: GatewayItem): gateway_repo.add(item) return {"message": "OK"} @router.put("/updateGateway", tags=["Reslevis"], dependencies=[Depends(get_current_user)]) def updateGateway(item: GatewayItem): gateway_repo.update(item) return {"message": "OK"} @router.delete("/removeGateway/{gateway_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)]) def removeGateway(gateway_id: str): gateway_repo.remove(gateway_id) return {"message": "OK"} @router.get( "/getBuildings", response_model=List[BuildingItem], tags=["Reslevis"], dependencies=[Depends(get_current_user)], ) def getBuildings(): return building_repo.list() @router.post("/postBuilding", tags=["Reslevis"], dependencies=[Depends(get_current_user)]) def postBuilding(item: BuildingItem): building_repo.add(item) return {"message": "OK"} @router.put("/updateBuilding", tags=["Reslevis"], dependencies=[Depends(get_current_user)]) def updateBuilding(item: BuildingItem): building_repo.update(item) return {"message": "OK"} @router.delete("/removeBuilding/{building_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)]) def removeBuilding(building_id: str): building_repo.remove(building_id) return {"message": "OK"} @router.get( "/getFloors", response_model=List[FloorItem], tags=["Reslevis"], dependencies=[Depends(get_current_user)], ) def getFloors(): return floor_repo.list() @router.post("/postFloor", tags=["Reslevis"], dependencies=[Depends(get_current_user)]) def postFloor(item: FloorItem): floor_repo.add(item) return {"message": "OK"} @router.put("/updateFloor", tags=["Reslevis"], dependencies=[Depends(get_current_user)]) def updateFloor(item: FloorItem): floor_repo.update(item) return {"message": "OK"} @router.delete("/removeFloor/{floor_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)]) def removeFloor(floor_id: str): floor_repo.remove(floor_id) return {"message": "OK"} @router.get( "/getZones", response_model=List[ZoneItem], tags=["Reslevis"], dependencies=[Depends(get_current_user)], ) def getZones(): return zone_repo.list() @router.post("/postZone", tags=["Reslevis"], dependencies=[Depends(get_current_user)]) def postZone(item: ZoneItem): zone_repo.add(item) return {"message": "OK"} @router.put("/updateZone", tags=["Reslevis"], dependencies=[Depends(get_current_user)]) def updateZone(item: ZoneItem): zone_repo.update(item) return {"message": "OK"} @router.delete("/removeZone/{zone_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)]) def removeZone(zone_id: str): zone_repo.remove(zone_id) return {"message": "OK"} @router.get( "/getZoneAreaDefinitions", response_model=List[ZoneAreaDefinitionItem], tags=["Reslevis"], dependencies=[Depends(get_current_user)], ) def getZoneAreaDefinitions(UUID: str | None = None): return zone_area_definition_repo.list(UUID) @router.post("/postZoneAreaDefinition", tags=["Reslevis"], dependencies=[Depends(get_current_user)]) def postZoneAreaDefinition(item: ZoneAreaDefinitionItem): zone_area_definition_repo.add(item) return {"message": "OK"} @router.put("/updateZoneAreaDefinition", tags=["Reslevis"], dependencies=[Depends(get_current_user)]) def updateZoneAreaDefinition(item: ZoneAreaDefinitionItem): zone_area_definition_repo.update(item) return {"message": "OK"} @router.delete("/removeZoneAreaDefinition/{zone_area_definition_uuid}", tags=["Reslevis"], dependencies=[Depends(get_current_user)]) def removeZoneAreaDefinition(zone_area_definition_uuid: str): zone_area_definition_repo.remove(zone_area_definition_uuid) return {"message": "OK"} @router.get( "/getTrackers", response_model=List[TrackerItem], tags=["Reslevis"], dependencies=[Depends(get_current_user)], ) async def getTrackers(): return await get_mode_aware_trackers( tracker_repo, SETTINGS_CORE_BASE_URL, config_env.BLE_AI_INFER_CSV, CORE_TIMEOUT, ) @router.post("/postTracker", tags=["Reslevis"], dependencies=[Depends(get_current_user)]) def postTracker(item: TrackerItem): tracker_repo.add(item) return {"message": "OK"} @router.put("/updateTracker", tags=["Reslevis"], dependencies=[Depends(get_current_user)]) def updateTracker(item: TrackerItem): tracker_repo.update(item) return {"message": "OK"} @router.delete("/removeTracker/{tracker_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)]) def removeTracker(tracker_id: str): tracker_repo.remove(tracker_id) return {"message": "OK"} @router.get( "/getTrackerZones", response_model=List[TrackerZoneItem], tags=["Reslevis"], dependencies=[Depends(get_current_user)], ) def getTrackerZones(): return tracker_zone_repo.list() @router.post("/postTrackerZone", tags=["Reslevis"], dependencies=[Depends(get_current_user)]) def postTrackerZone(item: TrackerZoneItem): tracker_zone_repo.add(item) return {"message": "OK"} @router.put("/updateTrackerZone", tags=["Reslevis"], dependencies=[Depends(get_current_user)]) def updateTrackerZone(item: TrackerZoneItem): tracker_zone_repo.update(item) return {"message": "OK"} @router.delete("/removeTrackerZone/{tracker_zone_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)]) def removeTrackerZone(tracker_zone_id: str): tracker_zone_repo.remove(tracker_zone_id) return {"message": "OK"} @router.get( "/getTracks", response_model=List[TrackHistoryItem], tags=["Reslevis"], dependencies=[Depends(get_current_user)], ) async def getTracks( tracker_id: str = Query(..., alias="id"), limit: Optional[int] = Query(None, ge=1), from_: Optional[str] = Query(None, alias="from"), to: Optional[str] = Query(None), ): params = {} if limit is not None: params["limit"] = limit if from_: params["from"] = from_ if to: params["to"] = to return await _fetch_tracks_for_tracker(tracker_id, params) @router.get( "/getTracks/{tracker_id}", response_model=List[TrackHistoryItem], tags=["Reslevis"], dependencies=[Depends(get_current_user)], ) async def getTrack( tracker_id: str, limit: Optional[int] = Query(None, ge=1), from_: Optional[str] = Query(None, alias="from"), to: Optional[str] = Query(None), ): params = {} if limit is not None: params["limit"] = limit if from_: params["from"] = from_ if to: params["to"] = to return await _fetch_tracks_for_tracker(tracker_id, params) @router.post("/postTrack", tags=["Reslevis"], dependencies=[Depends(get_current_user)]) def postTrack(item: TrackItem): track_repo.add(item) return {"message": "OK"} @router.put("/updateTrack", tags=["Reslevis"], dependencies=[Depends(get_current_user)]) def updateTrack(item: TrackItem): track_repo.update(item) return {"message": "OK"} @router.delete("/removeTrack/{track_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)]) def removeTrack(track_id: str): track_repo.remove(track_id) return {"message": "OK"} @router.get( "/getAlarms", response_model=List[AlarmCoreItem], tags=["Reslevis"], dependencies=[Depends(get_current_user)], ) async def getAlarms(): async with httpx.AsyncClient(timeout=CORE_TIMEOUT) as client: resp = await client.get(f"{ALERTS_CORE_BASE_URL}/reslevis/alerts") if resp.status_code >= 400: detail = resp.text.strip() or "CORE alerts request failed" raise HTTPException(status_code=resp.status_code, detail=detail) try: payload = resp.json() except ValueError as exc: raise HTTPException(status_code=502, detail="Invalid CORE response") from exc if not isinstance(payload, list): raise HTTPException(status_code=502, detail="Unexpected CORE response type") return payload @router.put( "/updateAlarm", tags=["Reslevis"], dependencies=[Depends(get_current_user)], ) async def updateAlarm(item: AlarmStatusUpdateItem): async with httpx.AsyncClient(timeout=CORE_TIMEOUT) as client: resp = await client.patch( f"{ALERTS_CORE_BASE_URL}/reslevis/alerts/{item.id}", json={"status": item.status}, ) if resp.status_code >= 400: detail = resp.text.strip() or "CORE alert update failed" raise HTTPException(status_code=resp.status_code, detail=detail) if not resp.content: return {"message": "OK"} try: return resp.json() except ValueError: return {"message": "OK"} @router.get( "/getOperators", response_model=List[OperatorItem], tags=["Reslevis"], dependencies=[Depends(get_current_user)], ) def getOperators(): return operator_repo.list() @router.post("/postOperator", tags=["Reslevis"], dependencies=[Depends(get_current_user)]) def postOperator(item: OperatorItem): operator_repo.add(item) return {"message": "OK"} @router.put("/updateOperator", tags=["Reslevis"], dependencies=[Depends(get_current_user)]) def updateOperator(item: OperatorItem): operator_repo.update(item) return {"message": "OK"} @router.delete("/removeOperator/{operator_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)]) def removeOperator(operator_id: str): operator_repo.remove(operator_id) return {"message": "OK"} @router.get( "/getSubjects", response_model=List[SubjectItem], tags=["Reslevis"], dependencies=[Depends(get_current_user)], ) def getSubjects(): return subject_repo.list() @router.post("/postSubject", tags=["Reslevis"], dependencies=[Depends(get_current_user)]) def postSubject(item: SubjectItem): subject_repo.add(item) return {"message": "OK"} @router.put("/updateSubject", tags=["Reslevis"], dependencies=[Depends(get_current_user)]) def updateSubject(item: SubjectItem): subject_repo.update(item) return {"message": "OK"} @router.delete("/removeSubject/{subject_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)]) def removeSubject(subject_id: str): subject_repo.remove(subject_id) return {"message": "OK"} @router.get( "/getSettings", response_model=List[SettingItem], tags=["Reslevis"], dependencies=[Depends(get_current_user)], ) def getSettings(): return setting_repo.list() @router.post("/postSetting", tags=["Reslevis"], dependencies=[Depends(get_current_user)]) def postSetting(item: SettingItem): setting_repo.add(item) return {"message": "OK"} @router.put("/updateSetting", tags=["Reslevis"], dependencies=[Depends(get_current_user)]) def updateSetting(item: SettingItem): setting_repo.update(item) return {"message": "OK"} @router.delete("/removeSetting/{setting_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)]) def removeSetting(setting_id: str): setting_repo.remove(setting_id) return {"message": "OK"} @router.get( "/getGuiConfigs", response_model=List[GuiConfigItem], tags=["Reslevis"], dependencies=[Depends(get_current_user)], ) def getGuiConfigs(): return gui_config_repo.list() @router.post("/postGuiConfig", tags=["Reslevis"], dependencies=[Depends(get_current_user)]) def postGuiConfig(item: GuiConfigItem): gui_config_repo.add(item) return {"message": "OK"} @router.put("/updateGuiConfig", tags=["Reslevis"], dependencies=[Depends(get_current_user)]) def updateGuiConfig(item: GuiConfigItem): gui_config_repo.update(item) return {"message": "OK"} @router.delete("/removeGuiConfig/{gui_config_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)]) def removeGuiConfig(gui_config_id: str): gui_config_repo.remove(gui_config_id) return {"message": "OK"} @router.get( "/getCoreSettings", response_model=List[CoreSettingsItem], tags=["Reslevis"], dependencies=[Depends(get_current_user)], ) async def getCoreSettings(): async with httpx.AsyncClient(timeout=CORE_TIMEOUT) as client: resp = await client.get(f"{SETTINGS_CORE_BASE_URL}/reslevis/settings") if resp.status_code >= 400: detail = resp.text.strip() or "CORE settings request failed" raise HTTPException(status_code=resp.status_code, detail=detail) try: payload = resp.json() except ValueError as exc: raise HTTPException(status_code=502, detail="Invalid CORE response") from exc if not isinstance(payload, list): raise HTTPException(status_code=502, detail="Unexpected CORE response type") return payload @router.put( "/updateCoreSettings", tags=["Reslevis"], dependencies=[Depends(get_current_user)], ) async def updateCoreSettings(item: CoreSettingsUpdateItem): async with httpx.AsyncClient(timeout=CORE_TIMEOUT) as client: resp = await client.patch( f"{SETTINGS_CORE_BASE_URL}/reslevis/settings", json=item.model_dump(exclude_none=True), ) if resp.status_code >= 400: detail = resp.text.strip() or "CORE settings update failed" raise HTTPException(status_code=resp.status_code, detail=detail) if not resp.content: return {"message": "OK"} try: return resp.json() except ValueError: return {"message": "OK"}