================================================================================
  Question 3: CoAP Air Quality Monitoring & Random Forest Classification [30 Marks]
================================================================================

FULL QUESTION:
CAE 2 IoT Practical Exam - Section B (Answer either Q3 or Q4):
Q3. CoAP-Based Air Quality Monitoring and Random Forest Classification [30 Marks]
Develop a CoAP-based air quality monitoring and classification system for Kochi.
Input API Link: https://air-quality-api.open-meteo.com/v1/air-quality?latitude=9.93&longitude=76.27&current=pm10,pm2_5,nitrogen_dioxide,sulphur_dioxide,ozone
Air-quality parameters: PM2.5, PM10, NO2, SO2, and O3.
Dataset: city_day.csv.
The system should:
- Develop a CoAP Server and CoAP Client. [6 Marks]
- Retrieve live air-quality data of Kochi via CoAP Server. [6 Marks]
- Transfer data to CoAP Client using CoAP GET request. [5 Marks]
- Develop a dashboard displaying PM2.5, PM10, NO2, SO2, and O3. [5 Marks]
- Train and apply Random Forest classification using historical city_day.csv dataset to classify into Good, Satisfactory, Moderate, Poor, Very Poor, Severe. [5 Marks]
- Display predicted air-quality classification on the dashboard. [3 Marks]
Flow: Air Quality API -> CoAP Server -> CoAP Client -> Dashboard -> Random Forest Classification.

SHORT QUESTION:
CoAP Air Quality Server & Client with Random Forest Classification and Dashboard [30 Marks].

================================================================================
README & SETUP INSTRUCTIONS:
================================================================================
# Question 3: CoAP-Based Air Quality Monitoring and Random Forest Classification

## Problem Statement [30 Marks]
Develop a CoAP-based air quality monitoring and classification system for Kochi.

- **Input API Link**:
  `https://air-quality-api.open-meteo.com/v1/air-quality?latitude=9.93&longitude=76.27&current=pm10,pm2_5,nitrogen_dioxide,sulphur_dioxide,ozone`
- **Air Quality Parameters**: PM2.5, PM10, NO₂, SO₂ and O₃
- **Historical Dataset**: Kaggle `city_day.csv` (included in this folder)
- **Model**: Random Forest Classifier
  - Input Features: `PM2.5`, `PM10`, `NO2`, `SO2`, `O3`
  - Target: `AQI_Bucket` (Good, Satisfactory, Moderate, Poor, Very Poor, Severe)
- **Live Ground Truth**: [https://app.cpcbccr.com/](https://app.cpcbccr.com/)

---

## Architectural Flow
```text
Air Quality API ──> CoAP Server (UDP 5683) ──> CoAP Client ──> Dashboard ──> Random Forest Classification
```

---

## Files in this Folder
- `server.py`: Asynchronous CoAP Server listening on port 5683 (`/airquality`), fetching live Kochi data from Open-Meteo REST API.
- `client.py`: Standalone CLI CoAP Client that fetches live readings, trains Random Forest model, and prints predictions to console.
- `app.py`: Full-featured Streamlit interactive web dashboard and CoAP Client.
- `city_day.csv`: Historical dataset containing air quality records across Indian cities.

---

## How to Run

### Step 1: Install Dependencies
```bash
pip install pandas scikit-learn streamlit aiocoap requests matplotlib
```

### Step 2: Start the CoAP Server
Open a terminal in the `Q3` directory:
```bash
python server.py
```
*Expected Server Output:*
```text
============================================================
  Kochi CoAP Air Quality Server Active on Port 5683
============================================================
Endpoint: coap://127.0.0.1:5683/airquality
Waiting for CoAP Client GET requests...
```

### Step 3: Run the CLI Client OR the Streamlit Dashboard

#### Option A: Run CLI Client
Open a second terminal in the `Q3` directory:
```bash
python client.py
```

#### Option B: Run Streamlit Dashboard
```bash
streamlit run app.py
```
Open your browser at `http://localhost:8501`. Click **"Fetch Live Air Quality via CoAP"** to trigger a CoAP GET transaction and view live predictions and model metrics!

================================================================================
SOURCE CODE FILES:
================================================================================

1. server.py
--------------------------------------------------------------------------------
import asyncio
import json
import requests
from aiocoap import Context, Message, resource

AIR_QUALITY_URL = (
    "https://air-quality-api.open-meteo.com/v1/air-quality?"
    "latitude=9.93&longitude=76.27&current=pm10,pm2_5,nitrogen_dioxide,sulphur_dioxide,ozone"
)

class AirQualityResource(resource.Resource):

    def fetch_live_api(self):
        response = requests.get(AIR_QUALITY_URL, timeout=10)
        response.raise_for_status()
        return response.json().get("current", {})

    async def render_get(self, request):
        print(f"\n[CoAP Server] Received GET request from {request.remote}")
        try:

            current = await asyncio.to_thread(self.fetch_live_api)

            payload = {
                "status": "success",
                "city": "Kochi",
                "latitude": 9.93,
                "longitude": 76.27,
                "time": current.get("time"),
                "PM2.5": current.get("pm2_5"),
                "PM10": current.get("pm10"),
                "NO2": current.get("nitrogen_dioxide"),
                "SO2": current.get("sulphur_dioxide"),
                "O3": current.get("ozone"),
            }

            print("[CoAP Server] Serving live air quality parameters:")
            print(f"  PM2.5 : {payload['PM2.5']} ug/m3")
            print(f"  PM10  : {payload['PM10']} ug/m3")
            print(f"  NO2   : {payload['NO2']} ug/m3")
            print(f"  SO2   : {payload['SO2']} ug/m3")
            print(f"  O3    : {payload['O3']} ug/m3")

            encoded = json.dumps(payload).encode("utf-8")
            return Message(payload=encoded)

        except Exception as e:
            error_msg = json.dumps({"status": "error", "error": str(e)}).encode("utf-8")
            print(f"[CoAP Server] Error fetching API: {e}")
            return Message(payload=error_msg)

async def main():
    root = resource.Site()
    root.add_resource(["airquality"], AirQualityResource())

    await Context.create_server_context(root, bind=("127.0.0.1", 5683))
    print("CoAP Server Active on port 5683")
    print("Endpoint: coap://127.0.0.1:5683/airquality")
    print("Waiting for CoAP Client GET requests (Press Ctrl+C to stop)...")
    await asyncio.get_running_loop().create_future()

if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        print("\n[CoAP Server] Stopped by user.")
--------------------------------------------------------------------------------

2. client.py
--------------------------------------------------------------------------------
import asyncio
import json
import os
import pandas as pd
from aiocoap import Context, Message, GET
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, classification_report

FEATURES = ["PM2.5", "PM10", "NO2", "SO2", "O3"]
TARGET = "AQI_Bucket"
SERVER_URI = "coap://127.0.0.1:5683/airquality"

async def fetch_air_quality_coap(uri=SERVER_URI):
    protocol = await Context.create_client_context()
    try:
        request = Message(code=GET, uri=uri)
        response = await protocol.request(request).response
        return json.loads(response.payload.decode("utf-8"))
    finally:
        await protocol.shutdown()

def train_random_forest(csv_path="city_day.csv"):
    if not os.path.exists(csv_path):
        raise FileNotFoundError(f"Dataset '{csv_path}' not found in current folder.")

    print(f"\n[1] Loading historical air quality data from '{csv_path}'...")
    df = pd.read_csv(csv_path)

    data = df[FEATURES + [TARGET]].dropna()
    print(f"    Loaded {len(data)} valid training records.")

    X = data[FEATURES]
    y = data[TARGET]

    X_train, X_test, y_train, y_test = train_test_split(
        X, y, test_size=0.20, random_state=42, stratify=y
    )

    print("[2] Training Random Forest Classifier (100 estimators)...")
    model = RandomForestClassifier(n_estimators=100, random_state=42)
    model.fit(X_train, y_train)

    y_pred = model.predict(X_test)
    accuracy = accuracy_score(y_test, y_pred)
    print(f"    Model Accuracy: {accuracy * 100:.2f}%\n")
    print("Classification Summary:")
    print(classification_report(y_test, y_pred))

    return model, accuracy

def main():

    print(f"\n[Step 1] Connecting to CoAP Server at {SERVER_URI}...")
    try:
        coap_data = asyncio.run(fetch_air_quality_coap())
        if coap_data.get("status") == "error":
            print(f"[Error from Server]: {coap_data.get('error')}")
            return
    except Exception as e:
        print(f"[ERROR] Could not connect to CoAP Server on 127.0.0.1:5683: {e}")
        print("Please ensure 'python server.py' is running in another terminal.")
        return

    print("\n[Step 2] Live Air Quality Telemetry Received via CoAP:")
    print(f"  Location   : {coap_data.get('city')} (Lat: {coap_data.get('latitude')}, Lon: {coap_data.get('longitude')})")
    print(f"  Timestamp  : {coap_data.get('time')}")
    print(f"  PM2.5      : {coap_data.get('PM2.5')} ug/m3")
    print(f"  PM10       : {coap_data.get('PM10')} ug/m3")
    print(f"  NO2        : {coap_data.get('NO2')} ug/m3")
    print(f"  SO2        : {coap_data.get('SO2')} ug/m3")
    print(f"  O3         : {coap_data.get('O3')} ug/m3")

    model, _ = train_random_forest()

    input_sample = pd.DataFrame([{
        "PM2.5": float(coap_data["PM2.5"]),
        "PM10":  float(coap_data["PM10"]),
        "NO2":   float(coap_data["NO2"]),
        "SO2":   float(coap_data["SO2"]),
        "O3":    float(coap_data["O3"])
    }])

    predicted_category = model.predict(input_sample)[0]
    probabilities = model.predict_proba(input_sample)[0]

    print("=" * 70)
    print(f"  PREDICTED AIR QUALITY CATEGORY: >>> {predicted_category.upper()} <<<")
    print("=" * 70)
    print("Class Probabilities:")
    for cls, prob in zip(model.classes_, probabilities):
        print(f"  {cls:15s}: {prob * 100:6.2f}%")
    print("\nLive CPC BCCR Ground Truth Monitor: https://app.cpcbccr.com/")

if __name__ == "__main__":
    main()
--------------------------------------------------------------------------------

3. app.py
--------------------------------------------------------------------------------
import asyncio
import json
import streamlit as st
import pandas as pd
from aiocoap import Context, Message, GET
from sklearn.ensemble import RandomForestClassifier

st.title("Kochi Air Quality Monitoring & Random Forest")

@st.cache_resource
def get_model():
    df = pd.read_csv("city_day.csv")[["PM2.5", "PM10", "NO2", "SO2", "O3", "AQI_Bucket"]].dropna()
    X = df[["PM2.5", "PM10", "NO2", "SO2", "O3"]]
    y = df["AQI_Bucket"]
    model = RandomForestClassifier(n_estimators=100, random_state=42)
    model.fit(X, y)
    return model

model = get_model()

async def fetch_coap():
    ctx = await Context.create_client_context()
    req = Message(code=GET, uri="coap://127.0.0.1:5683/airquality")
    res = await ctx.request(req).response
    await ctx.shutdown()
    return json.loads(res.payload.decode("utf-8"))

if st.button("Fetch Live Air Quality via CoAP"):
    data = asyncio.run(fetch_coap())
    st.subheader("Retrieved Air Quality Parameters:")
    st.write(data)

    sample = pd.DataFrame([[data["PM2.5"], data["PM10"], data["NO2"], data["SO2"], data["O3"]]], 
                          columns=["PM2.5", "PM10", "NO2", "SO2", "O3"])
    prediction = model.predict(sample)[0]
    st.success(f"Predicted AQI Category: {prediction}")
--------------------------------------------------------------------------------
