You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

168 regels
6.7 KiB

  1. import streamlit as st
  2. import pandas as pd
  3. import os
  4. import json
  5. import folium
  6. import requests
  7. import yaml
  8. from streamlit_folium import st_folium
  9. from pathlib import Path
  10. from PIL import Image
  11. import base64
  12. from io import BytesIO
  13. # --- UTILS ---
  14. @st.cache_data
  15. def get_image_base64(img_path):
  16. img = Image.open(img_path).convert("RGBA")
  17. w, h = img.size
  18. buffered = BytesIO()
  19. img.save(buffered, format="PNG")
  20. img_str = base64.b64encode(buffered.getvalue()).decode("ascii")
  21. return f"data:image/png;base64,{img_str}", w, h
  22. def _norm_mac_internal(s: str) -> str:
  23. """Standardizza il MAC in formato xx:xx:xx:xx:xx:xx minuscolo per il matching."""
  24. s = (s or "").strip().replace("-", "").replace(":", "").replace(".", "").lower()
  25. if len(s) != 12: return s
  26. return ":".join([s[i:i+2] for i in range(0, 12, 2)])
  27. def fetch_beacons_metadata(cfg):
  28. """Recupera l'elenco dei beacon dalle API e normalizza i MAC per il lookup."""
  29. api_c = cfg.get("api", {})
  30. token_url = api_c.get("token_url")
  31. beacons_url = api_c.get("get_beacons_url")
  32. try:
  33. secrets_path = os.environ.get("SECRETS_FILE") or "/config/secrets.yaml"
  34. with open(secrets_path, "r") as f:
  35. sec = yaml.safe_load(f).get("oidc", {})
  36. # 1. Autenticazione OIDC
  37. payload = {
  38. "grant_type": "password",
  39. "client_id": api_c.get("client_id", "Fastapi"),
  40. "client_secret": sec.get("client_secret", ""),
  41. "username": sec.get("username", "core"),
  42. "password": sec.get("password", "")
  43. }
  44. resp_t = requests.post(token_url, data=payload, verify=False, timeout=5)
  45. if resp_t.status_code != 200: return {}
  46. token = resp_t.json().get("access_token")
  47. # 2. Download Beacon List e Normalizzazione
  48. headers = {"Authorization": f"Bearer {token}", "accept": "application/json"}
  49. resp_b = requests.get(beacons_url, headers=headers, verify=False, timeout=5)
  50. if resp_b.status_code == 200:
  51. # Dizionario {mac_normalizzato: nome_amichevole}
  52. return {
  53. _norm_mac_internal(it["mac"]): it.get("name", "N/A")
  54. for it in resp_b.json() if "mac" in it
  55. }
  56. except Exception as e:
  57. st.error(f"Errore recupero nomi beacon: {e}")
  58. return {}
  59. def show_inference_page(cfg):
  60. st.subheader("📡 Monitoraggio Beacon Real-Time")
  61. # --- CONFIGURAZIONE PERCORSI ---
  62. MAPS_DIR = Path(cfg['maps']['map_dir'])
  63. INFER_FILE = Path("/data/infer/infer.csv")
  64. # --- 1. SELEZIONE E STATO ---
  65. maps = sorted([f.replace(cfg['maps']['floor_prefix'], "").split('.')[0]
  66. for f in os.listdir(MAPS_DIR) if f.startswith(cfg['maps']['floor_prefix'])])
  67. if not maps:
  68. st.warning("Nessuna mappa configurata.")
  69. return
  70. df_infer = pd.DataFrame()
  71. if INFER_FILE.exists():
  72. df_infer = pd.read_csv(INFER_FILE, sep=";")
  73. c_piano, c_count, c_size = st.columns([3, 2, 2])
  74. with c_piano:
  75. sub1, sub2 = st.columns([1, 1.2])
  76. sub1.markdown("<p style='padding-top:35px; font-weight:bold; font-size:15px;'>Piano Visualizzato:</p>", unsafe_allow_html=True)
  77. floor_id = sub2.selectbox("", maps, key="inf_floor_v24", label_visibility="collapsed")
  78. df_active = df_infer[(df_infer['z'].astype(str) == str(floor_id)) & (df_infer['x'] != -1)] if not df_infer.empty else pd.DataFrame()
  79. with c_count:
  80. st.info(f"📡 Beacon Attivi: **{len(df_active)}**\n(Totali nel file: {len(df_infer)})")
  81. with c_size:
  82. m_size = st.slider("Dimensione Beacon:", 5, 20, 8, key="inf_msize_v24")
  83. meta_path = MAPS_DIR / f"{cfg['maps']['meta_prefix']}{floor_id}.json"
  84. if not meta_path.exists(): return
  85. with open(meta_path, "r") as f: meta = json.load(f)
  86. # --- 2. RENDERING MAPPA ---
  87. st.markdown("---")
  88. img_p = next((MAPS_DIR / f"{cfg['maps']['floor_prefix']}{floor_id}{e}" for e in ['.png','.jpg'] if (MAPS_DIR / f"{cfg['maps']['floor_prefix']}{floor_id}{e}").exists()))
  89. img_data, w, h = get_image_base64(img_p)
  90. bounds = [[0, 0], [h, w]]
  91. m = folium.Map(location=[h/2, w/2], crs="Simple", tiles=None, attribution_control=False)
  92. m.fit_bounds(bounds)
  93. m.options.update({"minZoom": -6, "maxZoom": 6, "zoomSnap": 0.25, "maxBounds": bounds, "maxBoundsViscosity": 1.0})
  94. folium.raster_layers.ImageOverlay(image=img_data, bounds=bounds).add_to(m)
  95. # Recupero nomi per etichette mappa
  96. names_map = fetch_beacons_metadata(cfg)
  97. if meta["calibrated"] and meta["origin"] != [0,0]:
  98. # Origine (Punto di riferimento)
  99. folium.CircleMarker(location=[meta["origin"][1], meta["origin"][0]], radius=4, color="black", fill=True).add_to(m)
  100. for _, row in df_active.iterrows():
  101. px_x = (row['x'] * meta["pixel_ratio"]) + meta["origin"][0]
  102. px_y = meta["origin"][1] - (row['y'] * meta["pixel_ratio"])
  103. # --- UMANIZZAZIONE ETICHETTA MAPPA ---
  104. norm_mac = _norm_mac_internal(row['mac'])
  105. # Se disponibile usa il nome da API, altrimenti le ultime cifre del MAC
  106. label_text = names_map.get(norm_mac, str(row['mac'])[-5:])
  107. # Pallino Beacon
  108. folium.CircleMarker(
  109. location=[px_y, px_x], radius=m_size, color="blue",
  110. fill=True, fill_color="cyan", fill_opacity=0.8,
  111. tooltip=f"Device: {label_text} | MAC: {row['mac']}"
  112. ).add_to(m)
  113. # Etichetta Nome (accanto al punto)
  114. folium.Marker(
  115. location=[px_y, px_x],
  116. icon=folium.DivIcon(html=f"""
  117. <div style="
  118. font-family: sans-serif;
  119. color: #0d47a1;
  120. font-weight: bold;
  121. white-space: nowrap;
  122. font-size: {int(m_size*1.1)}pt;
  123. transform: translate({m_size+2}px, -{m_size+2}px);
  124. text-shadow: 1px 1px 2px white;
  125. ">
  126. {label_text}
  127. </div>
  128. """)
  129. ).add_to(m)
  130. st_folium(m, height=700, width=None, key=f"inf_map_v24_{floor_id}", use_container_width=True)
  131. # --- 3. TABELLA RIEPILOGO COMPLETA (Arricchita con Nome API) ---
  132. if not df_infer.empty:
  133. st.subheader("Dettaglio Dispositivi (Tutti i Piani)")
  134. df_display = df_infer.copy()
  135. df_display['mac_norm'] = df_display['mac'].apply(_norm_mac_internal)
  136. df_display['name'] = df_display['mac_norm'].map(names_map).fillna("Sconosciuto")
  137. # Selezione e riordino colonne finali
  138. cols_to_show = ['mac', 'name', 'z', 'x', 'y']
  139. st.dataframe(df_display[cols_to_show], use_container_width=True)