# EDIAQI Air Quality Dashboard

## Indice

- [Panoramica](#panoramica)
- [Creazione ambiente virtuale e installazione dipendenze](#creazione-ambiente-virtuale-e-installazione-dipendenze)
- [Avvio Applicazione](#avvio-applicazione)
- [Architettura Dati](#architettura-dati)
- [Modalità di Visualizzazione](#modalità-di-visualizzazione)
  - [Visualizzazione Semplice](#visualizzazione-semplice-default)
  - [Visualizzazione Avanzata](#visualizzazione-avanzata-toggle-in-alto-a-destra)
  - [Mappa Sensori](#mappa-sensori-icona-️)
- [Calcolo Indice Qualità Aria (IAQ)](#calcolo-indice-qualità-aria-iaq)
  - [Principio](#principio)
  - [Processo di Calcolo](#processo-di-calcolo)
  - [Esempio Pratico](#esempio-pratico)
- [Funzionalità Chiave](#funzionalità-chiave)
- [Configurazione](#configurazione)

---

## Panoramica

Dashboard Streamlit per il monitoraggio della qualità dell'aria indoor negli edifici di Ferrara nell'ambito del progetto **EDIAQI** (finanziato da Horizon Europe).

Visualizza dati in tempo reale da sensori NetPID tramite server FROST (SensorThings API), analizzando **PM2.5**, **CO₂**, **NO₂**, **O₃**, **CO** e **TVOCs** secondo standard ISS/OMS per ambienti indoor Classe II.

---

## Creazione ambiente virtuale e installazione dipendenze
```bash
python -m venv venv
venv\Scripts\activate
pip install -r requirements.txt
```

## Avvio Applicazione

```bash
streamlit run widget_app_v5.py
```

- **Login**: Inserire credenziali FROST Server

---

## Architettura Dati

| Componente | Descrizione |
|------------|-------------|
| **Istituto/Edificio** | Raggruppamento logico (es. "Istituto Copernico", "Comune Ferrara") |
| **Thing (NetPID)** | Singolo sensore installato in un ambiente specifico (es. "aula 13") |
| **Datastream** | Misura di un inquinante (PM2.5, CO₂, NO₂, O₃, CO, TVOCs) |
| **Observation** | Valore misurato con timestamp (phenomenonTime) |
| **Location** | Informzioni sulla locazione del sensore |
| **Feature of Interest** | Dettagli ambiente (superficie, ventilazione, occupanti) |

---

## Modalità di Visualizzazione

### Visualizzazione Semplice (Default)

#### Colonna Sinistra:
- **Indice IAQ (0-100)**: Valore numerico con categoria colorata
- **Causa peggiore**: Inquinante responsabile dell'indice più alto
- **Barra IAQ**: Gradiente con marker posizionato sull'indice corrente

#### Colonna Destra:
- **Barre inquinanti**: Ogni datastream con:
  - Valore numerico + unità di misura
  - Barra colorata (sfondo categoria + riempimento proporzionale)
  - Categoria testuale (Buona → Pessima)
  - Icona identificativa

- **Selezione sensore**: Click sui bottoni classe (con icone stato qualità aria)

---

### Visualizzazione Avanzata (Toggle in alto a destra)

#### Controlli temporali:
- **24 ore / 7 giorni**: Finestre predefinite
- **Finestra custom**: Selezione date inizio/fine (max 40 giorni) con validazione range disponibile

#### Grafici inquinanti:
- **Scroll verticale**: Ogni inquinante ha sezione dedicata con:
  - **Colonna sx**: Valore + barra verticale + categoria
  - **Colonna dx**: Grafico Plotly con:
    - Fasce colorate soglie
    - Linea temporale valori
    - Gap automatici se >5min tra misure

#### Toggle Heatmap:
- Visualizza matrice temporale categorie (righe = inquinanti, colonne = tempo)
- Riga IAQ sintetica (max categoria istante per istante)

---

### Mappa Sensori (Icona 🗺️)

#### Vista OpenStreetMap:
- **Marker colorati**: Colore = stato qualità aria corrente
- **Cluster dinamici**: Raggruppamento automatico
- **Offset circolari**: Sensori stesse coordinate ridistribuiti in cerchio

#### Click marker:
- **Overlay dettagli**:
  - Heatmap categorica ultime 24h
  - Pulsanti: "🏠 Informazioni utili" (Feature of Interest), "📍 Dettagli Luogo" (Location), "📊 Avanzata"

- **Pulsante Avanzata**: Switch a visualizzazione avanzata per quel sensore
- **Torna Dashboard**: Ripristina stato precedente (istituto + classe + vista)

---

## Calcolo Indice Qualità Aria (IAQ)

### Principio

L'indice IAQ (0-100) rappresenta la **peggiore condizione** tra tutti gli inquinanti monitorati, garantendo che un singolo valore critico prevalga sul quadro complessivo.

### Processo di Calcolo

#### 1. Normalizzazione Inquinanti (0-100)

Per ogni inquinante con soglie `[S1, S2, S3, S4, S5]`:

**Fasce 0-80 (lineari):**

| Fascia | Range IAQ | Condizione | Formula |
|--------|-----------|------------|---------|
| **Buona** | 0-16 | valore ≤ S1 | `(valore / S1) × 16` |
| **Accettabile** | 16-32 | S1 < valore ≤ S2 | `16 + ((valore - S1) / (S2 - S1)) × 16` |
| **Mediocre** | 32-48 | S2 < valore ≤ S3 | `32 + ((valore - S2) / (S3 - S2)) × 16` |
| **Scadente** | 48-64 | S3 < valore ≤ S4 | `48 + ((valore - S3) / (S4 - S3)) × 16` |
| **Inaccettabile** | 64-80 | S4 < valore ≤ S5 | `64 + ((valore - S4) / (S5 - S4)) × 16` |

**Fascia 80-100 (logaritmica):**

- **Pessima** (80-100): valore > S5 → `80 + 8 × log₂(valore / S5)` (cap 100)
  - Ogni raddoppio oltre S5 aggiunge 8 punti
  - Esempio: valore = 2×S5 → IAQ = 88 | valore = 4×S5 → IAQ = 96


#### 2. Regole di Aggregazione

- **IAQ finale** = `MAX(indici normalizzati)` → La condizione peggiore domina
- **Valori nulli/negativi**: Indice = NULL, categoria = "Non disponibile"
- **Dati obsoleti** (>6h): Sensore marcato offline con icona ⚠️

#### 3. Mappatura Categorie

| IAQ | Categoria | Colore |
|-----|-----------|--------|
| 0-15 | Buona | Verde (#00f9e5) |
| 16-31 | Accettabile | Verde chiaro (#00d5a7) |
| 32-47 | Mediocre | Giallo (#ffb700) |
| 48-63 | Scadente | Arancione (#ff004f) |
| 64-79 | Inaccettabile | Rosso (#9e0035) |
| 80-100 | Pessima | Viola (#890082) |

#### 4. Identificazione Causa

L'inquinante con indice normalizzato più alto viene etichettato come **"causa"** e mostrato nella visualizzazione semplice (es. "a causa di CO₂").

### Esempio Pratico

**Datastream CO₂**: Soglie = `[750, 1000, 1200, 1800, 2400]` ppm

| Valore | Calcolo | Indice | Categoria |
|--------|---------|--------|-----------|
| 600 ppm | `(600/750)×16` | 12.8 | Buona |
| 850 ppm | `16+((850-750)/(1000-750))×16` | 22.4 | Accettabile |
| 1500 ppm | `48+((1500-1200)/(1800-1200))×16` | 56.0 | Scadente |
| 3000 ppm | `80+8×log₂(3000/2400)` | 83.3 | Pessima |


---

## Funzionalità Chiave

### Reference Time
- Timestamp ultimo dato disponibile per ogni Thing
- Usato come punto di riferimento per grafici/heatmap
- In finestra custom: sostituito con `end_date` selezionata

### Cache Osservazioni
- **ObservationsCache**: Riduce chiamate API ripetute
- Invalidata al cambio finestra temporale o sensore


### Warning Stati
- **⚠️ Dati vecchi**: Ultimo dato > 6 ore fa
- **Grigio "Offline"**: Marker mappa per sensori inattivi
- **"-- unità"**: Valore non disponibile (con barra grigia)

### Timezone
- **Configurabile**: `config2.yaml` → `timezone: 'Europe/Rome'` (o 'local', 'UTC')
- **Display**: Tutti timestamp convertiti al timezone selezionato
- **Label automatica**: "(ora locale Ferrara)" per Europe/Rome

---

## Configurazione

### config2.yaml

```yaml
# Endpoint FROST Server
endpoint: 'https://frost.labservice.it/FROST-Server/v1.1'

# URL standard set (soglie inquinanti)
standard_url: 'https://drive.google.com/uc?export=download&id=10OUKgoIHi5NE0dROSTOBz9TifQRwflcU'

# Timezone visualizzazione ('Europe/Rome', 'UTC', 'local')
timezone: 'Europe/Rome'

# Titolo pagina
page_title: 'FROST Widget'
page_icon: '📊'

# Abilita grafici
enable_charts: true
```

### Mappature Edifici (widget_app_v5.py)

```python
ISTITUTI = {
    "Istituto Copernico": [
        "THI.FE.019","THI.FE.020","THI.FE.021","THI.FE.023", "THI.FE.022","THI.FE.024"
    ],
    "Istituto Carducci": [
        "THI.FE.014","THI.FE.015","THI.FE.016","THI.FE.017","THI.FE.018"
    ],
    "Istituto Aleotti": [
        "THI.FE.010", "THI.FE.011", "THI.FE.012", "THI.FE.013"
    ],

    "Casa Verri": [
        "THI.FE.001"
    ],

    "Agenzia Mobilità Impianti Ferrara": [
        "THI.FE.002","THI.FE.003","THI.FE.004"
    ],

    "Comune Ferrara via Maverna": [
        "THI.FE.005","THI.FE.006","THI.FE.007","THI.FE.008",
    ],

    "Ristorante Iaia": [
        "THI.FE.009"
    ],

    "Centro Universitario Sportivo di Ferrara": [
        "THI.FE.025","THI.FE.026","THI.FE.027","THI.FE.028","THI.FE.029"
    ],

    "Comune Ferrara via Marconi": [
        "THI.FE.030","THI.FE.031","THI.FE.032","THI.FE.033","THI.FE.034","THI.FE.035"
    ],
    
    "Deda Next": ["THI.FE.036"]
}

CLASSI_NAMES = {
    "THI.FE.019":"aula 13",
    "THI.FE.020": "aula 11",
    "THI.FE.021": "aula 10",
    "THI.FE.022": "aula 5",
    "THI.FE.023": "aula 2",
    "THI.FE.024":"lab",
    "THI.FE.014":"aula 5",
    "THI.FE.015":"aula T2",
    "THI.FE.016":"aula 2",
    "THI.FE.017":"aula 18",
    "THI.FE.018":"aula 21",
    "THI.FE.010":"aula 1 CAT",
    "THI.FE.011":"ricreazione",
    "THI.FE.012":"aula 23",
    "THI.FE.013":"lab. inf. 3",
    "THI.FE.001":"casa Verri",
    "THI.FE.002":"Ufficio M.",
    "THI.FE.003":"Ufficio I.",
    "THI.FE.004":"Ufficio R.",
    "THI.FE.005":"Ufficio C.",
    "THI.FE.006":"Ufficio M.",
    "THI.FE.007":"primo piano",
    "THI.FE.008":"Ufficio G.",
    "THI.FE.030":"Public Relations",
    "THI.FE.031":"waiting zone",
    "THI.FE.032":"Ufficio T.",
    "THI.FE.033":"Ufficio B.",
    "THI.FE.034":"Ufficio C.",
    "THI.FE.035":"Ufficio P.",
    "THI.FE.025":"Fitness Gym 1",
    "THI.FE.026":"Cardio Gym",
    "THI.FE.027":"Fitness Gym 2",
    "THI.FE.028":"Reception",
    "THI.FE.029":"Spinning Gym",
    "THI.FE.036":"Reception",
    "THI.FE.009":"Ristorante Iaia",
    }
```

### Standard Set (JSON)

Definisce per ogni inquinante:
- **datastreamName**: Nome canonico (es. "PM2.5")
- **datastreamNameAliases**: Alias riconosciuti
- **categoryThresholds**: Array 5 soglie `[S1, S2, S3, S4, S5]`
- **greaterIsWorse**: `true` se valori alti = peggio (default), `false` per inversi
- **categoryColors**: Array 6 colori hex
- **displayLabel**: Nome visualizzato
