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

168 行
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)