| @@ -153,7 +153,11 @@ async def stop_mqtt_monitor(): | |||||
| async def local_then_core(request: Request, call_next): | async def local_then_core(request: Request, call_next): | ||||
| internal_core_proxy_paths = { | internal_core_proxy_paths = { | ||||
| "/reslevis/updateAlarm", | "/reslevis/updateAlarm", | ||||
| <<<<<<< HEAD | |||||
| "/reslevis/updateCoreSettings", | "/reslevis/updateCoreSettings", | ||||
| ======= | |||||
| "/reslevis/updateCoreSettings", | |||||
| >>>>>>> Tracker_bug_fix_20260429 | |||||
| } | } | ||||
| # only proxy CRUD for Reslevis (change prefix or methods if needed) | # only proxy CRUD for Reslevis (change prefix or methods if needed) | ||||
| if ( | if ( | ||||
| @@ -0,0 +1,124 @@ | |||||
| import csv | |||||
| import logging | |||||
| from typing import Any, Dict, List, Optional | |||||
| import httpx | |||||
| log = logging.getLogger(__name__) | |||||
| def _norm_mac(v: Any) -> str: | |||||
| if v is None: | |||||
| return "" | |||||
| return "".join(ch for ch in str(v).strip().lower() if ch.isalnum()) | |||||
| def _none_if_empty(v: Any) -> Any: | |||||
| return None if v in ("", None, 0, "0") else v | |||||
| def _str_or_none(v: Any) -> Optional[str]: | |||||
| if v in ("", None): | |||||
| return None | |||||
| if isinstance(v, (int, float, bool)): | |||||
| return str(v) | |||||
| return v | |||||
| def _normalize_tracker(row: dict) -> dict: | |||||
| row = dict(row) | |||||
| row["floor"] = _none_if_empty(row.get("floor")) | |||||
| row["building"] = _none_if_empty(row.get("building")) | |||||
| row["battery"] = _str_or_none(row.get("battery")) | |||||
| row["temperature"] = _str_or_none(row.get("temperature")) | |||||
| row["acceleration"] = _str_or_none(row.get("acceleration")) | |||||
| row["heartRate"] = _str_or_none(row.get("heartRate")) | |||||
| return row | |||||
| async def _fetch_algorithm(core_base_url: str, timeout: float) -> Optional[str]: | |||||
| try: | |||||
| async with httpx.AsyncClient(timeout=timeout) as client: | |||||
| resp = await client.get(f"{core_base_url}/reslevis/settings") | |||||
| if 200 <= resp.status_code < 300: | |||||
| payload = resp.json() | |||||
| if isinstance(payload, list) and payload: | |||||
| value = payload[0].get("current_algorithm") | |||||
| if value is not None: | |||||
| return str(value).lower() | |||||
| except (httpx.RequestError, ValueError): | |||||
| pass | |||||
| return None | |||||
| async def _filter_mode_trackers( | |||||
| tracker_repo, core_base_url: str, timeout: float | |||||
| ) -> List[Dict[str, Any]]: | |||||
| try: | |||||
| async with httpx.AsyncClient(timeout=timeout) as client: | |||||
| resp = await client.get(f"{core_base_url}/reslevis/getTrackers") | |||||
| if 200 <= resp.status_code < 300: | |||||
| data = resp.json() | |||||
| if isinstance(data, list): | |||||
| normalized = [_normalize_tracker(r) for r in data if isinstance(r, dict)] | |||||
| tracker_repo._write_all(normalized) | |||||
| return normalized | |||||
| except (httpx.RequestError, ValueError): | |||||
| pass | |||||
| return tracker_repo.list() | |||||
| def _read_infer_positions(infer_csv_path: str) -> Dict[str, Dict[str, Optional[float]]]: | |||||
| positions: Dict[str, Dict[str, Optional[float]]] = {} | |||||
| try: | |||||
| with open(infer_csv_path, newline="") as f: | |||||
| reader = csv.DictReader(f, delimiter=";") | |||||
| for row in reader: | |||||
| mac = _norm_mac(row.get("mac")) | |||||
| if not mac: | |||||
| continue | |||||
| try: | |||||
| positions[mac] = { | |||||
| "x": int(row["x"]) if row.get("x") not in (None, "") else None, | |||||
| "y": int(row["y"]) if row.get("y") not in (None, "") else None, | |||||
| } | |||||
| except (KeyError, ValueError): | |||||
| continue | |||||
| except OSError: | |||||
| log.warning("BLE-AI infer CSV not found: %s", infer_csv_path) | |||||
| return positions | |||||
| def _ai_mode_trackers( | |||||
| tracker_repo, infer_csv_path: str | |||||
| ) -> List[Dict[str, Any]]: | |||||
| trackers = tracker_repo.list() | |||||
| positions = _read_infer_positions(infer_csv_path) | |||||
| if not positions: | |||||
| return trackers | |||||
| result = [] | |||||
| for tracker in trackers: | |||||
| t = dict(tracker) | |||||
| mac = _norm_mac(t.get("mac")) | |||||
| if mac and mac in positions: | |||||
| t["x"] = positions[mac]["x"] | |||||
| t["y"] = positions[mac]["y"] | |||||
| result.append(t) | |||||
| return result | |||||
| async def get_mode_aware_trackers( | |||||
| tracker_repo, | |||||
| core_base_url: str, | |||||
| infer_csv_path: str, | |||||
| timeout: float, | |||||
| ) -> List[Dict[str, Any]]: | |||||
| algorithm = await _fetch_algorithm(core_base_url, timeout) | |||||
| if algorithm == "filter": | |||||
| return await _filter_mode_trackers(tracker_repo, core_base_url, timeout) | |||||
| if algorithm == "ai": | |||||
| return _ai_mode_trackers(tracker_repo, infer_csv_path) | |||||
| return tracker_repo.list() | |||||
| @@ -41,6 +41,7 @@ from logica_reslevis.subject import SubjectJsonRepository | |||||
| from logica_reslevis.alarm import AlarmJsonRepository | from logica_reslevis.alarm import AlarmJsonRepository | ||||
| from logica_reslevis.track import TrackJsonRepository | from logica_reslevis.track import TrackJsonRepository | ||||
| from logica_reslevis.tracker_zone import TrackerZoneJsonRepository | from logica_reslevis.tracker_zone import TrackerZoneJsonRepository | ||||
| from logica_reslevis.tracker_mode import get_mode_aware_trackers | |||||
| from security import get_current_user | from security import get_current_user | ||||
| @@ -48,7 +49,11 @@ from security import get_current_user | |||||
| CORE_BASE_URL = config_env.CORE_API_URL.rstrip("/") | CORE_BASE_URL = config_env.CORE_API_URL.rstrip("/") | ||||
| ALERTS_CORE_BASE_URL = "http://localhost:1902" | ALERTS_CORE_BASE_URL = "http://localhost:1902" | ||||
| TRACKS_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://localhost:1902" | ||||
| ======= | |||||
| SETTINGS_CORE_BASE_URL = "http://127.0.0.1:1902" | |||||
| >>>>>>> Tracker_bug_fix_20260429 | |||||
| CORE_TIMEOUT = 2.0 # secondi | CORE_TIMEOUT = 2.0 # secondi | ||||
| async def sync_core_get(request: Request) -> None: | async def sync_core_get(request: Request) -> None: | ||||
| @@ -126,16 +131,6 @@ def _normalize_gateway(row: dict) -> dict: | |||||
| row["building"] = _none_if_empty(row.get("building")) | row["building"] = _none_if_empty(row.get("building")) | ||||
| return row | return row | ||||
| def _normalize_tracker(row: dict) -> dict: | |||||
| row = dict(row) | |||||
| row["floor"] = _none_if_empty(row.get("floor")) | |||||
| row["building"] = _none_if_empty(row.get("building")) | |||||
| row["battery"] = _str_or_none(row.get("battery")) | |||||
| row["temperature"] = _str_or_none(row.get("temperature")) | |||||
| row["acceleration"] = _str_or_none(row.get("acceleration")) | |||||
| row["heartRate"] = _str_or_none(row.get("heartRate")) | |||||
| return row | |||||
| def _normalize_track(row: dict) -> dict: | def _normalize_track(row: dict) -> dict: | ||||
| row = dict(row) | row = dict(row) | ||||
| row["ID"] = row.get("ID") | row["ID"] = row.get("ID") | ||||
| @@ -167,7 +162,6 @@ def _normalize_zone(row: dict) -> dict: | |||||
| CORE_GET_SYNC = { | CORE_GET_SYNC = { | ||||
| "/reslevis/getGateways": (gateway_repo, _normalize_gateway), | "/reslevis/getGateways": (gateway_repo, _normalize_gateway), | ||||
| "/reslevis/getZones": (zone_repo, _normalize_zone), | "/reslevis/getZones": (zone_repo, _normalize_zone), | ||||
| "/reslevis/getTrackers": (tracker_repo, _normalize_tracker), | |||||
| } | } | ||||
| @@ -352,8 +346,13 @@ def removeZoneAreaDefinition(zone_area_definition_uuid: str): | |||||
| tags=["Reslevis"], | tags=["Reslevis"], | ||||
| dependencies=[Depends(get_current_user)], | dependencies=[Depends(get_current_user)], | ||||
| ) | ) | ||||
| def getTrackers(): | |||||
| return tracker_repo.list() | |||||
| 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)]) | @router.post("/postTracker", tags=["Reslevis"], dependencies=[Depends(get_current_user)]) | ||||
| @@ -25,7 +25,10 @@ class FloorItem(BaseModel): | |||||
| class ZoneItem(BaseModel): | class ZoneItem(BaseModel): | ||||
| id: UUID | id: UUID | ||||
| name: str | name: str | ||||
| <<<<<<< HEAD | |||||
| groups: Optional[List[UUID]] = None | groups: Optional[List[UUID]] = None | ||||
| ======= | |||||
| >>>>>>> Tracker_bug_fix_20260429 | |||||
| floor: Optional[UUID] = None | floor: Optional[UUID] = None | ||||
| building: Optional[UUID] = None | building: Optional[UUID] = None | ||||
| @@ -53,6 +56,7 @@ class GatewayItem(BaseModel): | |||||
| notes: Optional[str] = None | notes: Optional[str] = None | ||||
| floor: Optional[UUID] = None | floor: Optional[UUID] = None | ||||
| building: Optional[UUID] = None | building: Optional[UUID] = None | ||||
| zone: Optional[UUID] = None | |||||
| class TrackerItem(BaseModel): | class TrackerItem(BaseModel): | ||||
| id: UUID | id: UUID | ||||
| @@ -83,6 +87,29 @@ class SettingItem(BaseModel): | |||||
| language: str | language: str | ||||
| class CoreSettingsItem(BaseModel): | |||||
| ID: int | |||||
| current_algorithm: str | |||||
| location_confidence: int | |||||
| last_seen_threshold: int | |||||
| beacon_metric_size: int | |||||
| HA_send_interval: int | |||||
| HA_send_changes_only: bool | |||||
| RSSI_enforce_threshold: bool | |||||
| RSSI_min_threshold: int | |||||
| class CoreSettingsUpdateItem(BaseModel): | |||||
| current_algorithm: str | |||||
| last_seen_threshold: int | |||||
| beacon_metric_size: int | |||||
| HA_send_interval: int | |||||
| HA_send_changes_only: bool | |||||
| RSSI_enforce_threshold: bool | |||||
| RSSI_min_threshold: int | |||||
| location_confidence: Optional[int] = None | |||||
| class GuiConfigItem(BaseModel): | class GuiConfigItem(BaseModel): | ||||
| id: str | id: str | ||||
| name: Optional[str] = None | name: Optional[str] = None | ||||
| @@ -132,6 +159,8 @@ class AlarmCoreItem(BaseModel): | |||||
| type: str | type: str | ||||
| status: str | status: str | ||||
| timestamp: str | timestamp: str | ||||
| operator: Optional[UUID] = None | |||||
| resolution_timestamp: Optional[str] = None | |||||
| class AlarmStatusUpdateItem(BaseModel): | class AlarmStatusUpdateItem(BaseModel): | ||||
| @@ -178,7 +207,12 @@ class TrackHistoryItem(BaseModel): | |||||
| class TrackerZoneItem(BaseModel): | class TrackerZoneItem(BaseModel): | ||||
| id: UUID | id: UUID | ||||
| <<<<<<< HEAD | |||||
| name: str | name: str | ||||
| ======= | |||||
| name: Optional[str] = None | |||||
| zoneList: List[UUID] | |||||
| >>>>>>> Tracker_bug_fix_20260429 | |||||
| tracker: UUID | tracker: UUID | ||||
| zoneList: list[UUID] | zoneList: list[UUID] | ||||
| days: Optional[str] = None | days: Optional[str] = None | ||||