iotlabkit 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,338 @@
1
+ """Template generator for Program 10: CoAP Environmental Monitoring (Open-Meteo + DB + Dashboard)."""
2
+
3
+ from typing import Dict
4
+ import os
5
+
6
+
7
+ def generate(target_dir: str, mode: str = "learning") -> Dict[str, str]:
8
+ files = {}
9
+
10
+ files["requirements.txt"] = (
11
+ "requests>=2.28.0\n"
12
+ "python-dotenv>=1.0.0\n"
13
+ "streamlit>=1.25.0\n"
14
+ )
15
+
16
+ files[".env.example"] = (
17
+ "# Environmental Station Coordinates (Default: Kochi/Kalamassery, Kerala)\n"
18
+ "LATITUDE=10.0159\n"
19
+ "LONGITUDE=76.3419\n\n"
20
+ "# CoAP Server Port\n"
21
+ "COAP_HOST=127.0.0.1\n"
22
+ "COAP_PORT=56830\n\n"
23
+ "# Cloud Database Configuration ('supabase' or 'firebase')\n"
24
+ "CLOUD_PROVIDER=supabase\n"
25
+ "SUPABASE_URL=https://your-project-ref.supabase.co\n"
26
+ "SUPABASE_KEY=your-supabase-anon-key\n"
27
+ "FIREBASE_DATABASE_URL=https://your-firebase-project.firebaseio.com\n"
28
+ )
29
+
30
+ files[".gitignore"] = (
31
+ ".env\n"
32
+ "__pycache__/\n"
33
+ "*.py[cod]\n"
34
+ "data/local_coap_env.json\n"
35
+ )
36
+
37
+ # Embed coap_core.py
38
+ coap_util_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "coap_util.py")
39
+ if os.path.exists(coap_util_path):
40
+ with open(coap_util_path, "r", encoding="utf-8") as f:
41
+ files["coap_core.py"] = f.read()
42
+
43
+ files["supabase_schema.sql"] = (
44
+ "-- Supabase SQL Table Schema for Program 10 (CoAP Environmental Monitoring)\n"
45
+ "CREATE TABLE IF NOT EXISTS coap_environmental_records (\n"
46
+ " id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,\n"
47
+ " created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::text, NOW()) NOT NULL,\n"
48
+ " latitude DOUBLE PRECISION NOT NULL,\n"
49
+ " longitude DOUBLE PRECISION NOT NULL,\n"
50
+ " temperature DOUBLE PRECISION,\n"
51
+ " humidity DOUBLE PRECISION,\n"
52
+ " pm10 DOUBLE PRECISION,\n"
53
+ " pm2_5 DOUBLE PRECISION,\n"
54
+ " carbon_monoxide DOUBLE PRECISION,\n"
55
+ " nitrogen_dioxide DOUBLE PRECISION,\n"
56
+ " ozone DOUBLE PRECISION,\n"
57
+ " european_aqi INTEGER\n"
58
+ ");\n\n"
59
+ "ALTER TABLE coap_environmental_records ENABLE ROW LEVEL SECURITY;\n"
60
+ "CREATE POLICY \"Allow anon read\" ON coap_environmental_records FOR SELECT USING (true);\n"
61
+ "CREATE POLICY \"Allow anon insert\" ON coap_environmental_records FOR INSERT WITH CHECK (true);\n"
62
+ )
63
+
64
+ # coap_env_server.py
65
+ if mode == "learning":
66
+ srv_doc = (
67
+ '"""CoAP Environmental Monitoring Server - Program 10.\n\n'
68
+ "Retrieves authentic real-time weather and air-quality observations from\n"
69
+ "Open-Meteo REST APIs and serves the consolidated metrics over CoAP UDP.\n"
70
+ '"""\n'
71
+ )
72
+ else:
73
+ srv_doc = '"""CoAP Environmental Server - Program 10 (Exam Mode)."""\n'
74
+
75
+ files["coap_env_server.py"] = srv_doc + (
76
+ "import argparse\n"
77
+ "import json\n"
78
+ "import os\n"
79
+ "import sys\n"
80
+ "import time\n"
81
+ "import requests\n"
82
+ "from dotenv import load_dotenv\n"
83
+ "from coap_core import CoAPServer, CODE_CONTENT, CODE_INTERNAL_ERR\n\n"
84
+ "load_dotenv()\n\n"
85
+ "DEFAULT_LAT = float(os.getenv('LATITUDE', 10.0159))\n"
86
+ "DEFAULT_LON = float(os.getenv('LONGITUDE', 76.3419))\n"
87
+ "COAP_HOST = os.getenv('COAP_HOST', '127.0.0.1')\n"
88
+ "COAP_PORT = int(os.getenv('COAP_PORT', 56830))\n\n"
89
+ "def fetch_live_environmental_data(lat: float, lon: float) -> dict:\n"
90
+ " weather_url = f\"https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lon}&current=temperature_2m,relative_humidity_2m,weather_code\"\n"
91
+ " air_url = f\"https://air-quality-api.open-meteo.com/v1/air-quality?latitude={lat}&longitude={lon}&current=pm10,pm2_5,carbon_monoxide,nitrogen_dioxide,ozone,european_aqi\"\n\n"
92
+ " record = {\n"
93
+ " 'latitude': lat,\n"
94
+ " 'longitude': lon,\n"
95
+ " 'timestamp': int(time.time()),\n"
96
+ " 'temperature': None,\n"
97
+ " 'humidity': None,\n"
98
+ " 'pm10': None,\n"
99
+ " 'pm2_5': None,\n"
100
+ " 'carbon_monoxide': None,\n"
101
+ " 'nitrogen_dioxide': None,\n"
102
+ " 'ozone': None,\n"
103
+ " 'european_aqi': None,\n"
104
+ " 'status': 'DATA_OK'\n"
105
+ " }\n\n"
106
+ " # 1. Fetch Weather\n"
107
+ " try:\n"
108
+ " w_resp = requests.get(weather_url, timeout=5)\n"
109
+ " if w_resp.status_code == 200:\n"
110
+ " curr_w = w_resp.json().get('current', {})\n"
111
+ " record['temperature'] = curr_w.get('temperature_2m')\n"
112
+ " record['humidity'] = curr_w.get('relative_humidity_2m')\n"
113
+ " except requests.exceptions.Timeout:\n"
114
+ " record['status'] = 'WEATHER_TIMEOUT'\n"
115
+ " except Exception as e:\n"
116
+ " record['status'] = f'WEATHER_ERROR: {e}'\n\n"
117
+ " # 2. Fetch Air Quality\n"
118
+ " try:\n"
119
+ " a_resp = requests.get(air_url, timeout=5)\n"
120
+ " if a_resp.status_code == 200:\n"
121
+ " curr_a = a_resp.json().get('current', {})\n"
122
+ " record['pm10'] = curr_a.get('pm10')\n"
123
+ " record['pm2_5'] = curr_a.get('pm2_5')\n"
124
+ " record['carbon_monoxide'] = curr_a.get('carbon_monoxide')\n"
125
+ " record['nitrogen_dioxide'] = curr_a.get('nitrogen_dioxide')\n"
126
+ " record['ozone'] = curr_a.get('ozone')\n"
127
+ " record['european_aqi'] = curr_a.get('european_aqi')\n"
128
+ " except requests.exceptions.Timeout:\n"
129
+ " record['status'] = 'AIR_QUALITY_TIMEOUT'\n"
130
+ " except Exception as e:\n"
131
+ " record['status'] = f'AIR_ERROR: {e}'\n\n"
132
+ " return record\n\n"
133
+ "def main():\n"
134
+ " parser = argparse.ArgumentParser(description='CoAP Environmental Monitoring Server')\n"
135
+ " parser.add_argument('--lat', type=float, default=DEFAULT_LAT, help='Latitude coordinate')\n"
136
+ " parser.add_argument('--lon', type=float, default=DEFAULT_LON, help='Longitude coordinate')\n"
137
+ " parser.add_argument('--port', type=int, default=COAP_PORT, help='CoAP UDP port')\n"
138
+ " args = parser.parse_args()\n\n"
139
+ " server = CoAPServer(host=COAP_HOST, port=args.port)\n\n"
140
+ " def live_resource_handler():\n"
141
+ " data = fetch_live_environmental_data(args.lat, args.lon)\n"
142
+ " return CODE_CONTENT, json.dumps(data)\n\n"
143
+ " server.add_resource('/environmental/live', live_resource_handler)\n\n"
144
+ " print('=' * 65)\n"
145
+ " print('🌐 Program 10: CoAP Environmental Monitoring Server')\n"
146
+ " print('=' * 65)\n"
147
+ " print(f'Listening on : udp://{COAP_HOST}:{args.port}')\n"
148
+ " print(f'CoAP Endpoint : coap://{COAP_HOST}:{args.port}/environmental/live')\n"
149
+ " print(f'Target Location : Lat={args.lat}, Lon={args.lon} (Kochi, Kerala)')\n"
150
+ " print('Live API Sources : Open-Meteo Weather & Air Quality APIs')\n"
151
+ " print('=' * 65)\n"
152
+ " print('Server started! Run `python coap_env_client.py` in Terminal 2.')\n"
153
+ " print('Press Ctrl+C to stop.')\n\n"
154
+ " try:\n"
155
+ " server.start(blocking=True)\n"
156
+ " except KeyboardInterrupt:\n"
157
+ " print('\\nShutting down CoAP server...')\n"
158
+ " server.stop()\n\n"
159
+ "if __name__ == '__main__':\n"
160
+ " main()\n"
161
+ )
162
+
163
+ # coap_env_client.py
164
+ if mode == "learning":
165
+ cli_doc = (
166
+ '"""CoAP Environmental Monitoring Client - Program 10.\n\n'
167
+ "Retrieves live environmental metrics from CoAP server over UDP and\n"
168
+ "persists records into Supabase / Firebase cloud database.\n"
169
+ '"""\n'
170
+ )
171
+ else:
172
+ cli_doc = '"""CoAP Environmental Client - Program 10 (Exam Mode)."""\n'
173
+
174
+ files["coap_env_client.py"] = cli_doc + (
175
+ "import argparse\n"
176
+ "import json\n"
177
+ "import os\n"
178
+ "import sys\n"
179
+ "import time\n"
180
+ "from datetime import datetime, timezone\n"
181
+ "import requests\n"
182
+ "from dotenv import load_dotenv\n"
183
+ "from coap_core import CoAPClient\n\n"
184
+ "load_dotenv()\n\n"
185
+ "COAP_HOST = os.getenv('COAP_HOST', '127.0.0.1')\n"
186
+ "COAP_PORT = int(os.getenv('COAP_PORT', 56830))\n"
187
+ "CLOUD_PROVIDER = os.getenv('CLOUD_PROVIDER', 'supabase').lower().strip()\n"
188
+ "SUPABASE_URL = os.getenv('SUPABASE_URL', '').rstrip('/')\n"
189
+ "SUPABASE_KEY = os.getenv('SUPABASE_KEY', '')\n"
190
+ "FIREBASE_URL = os.getenv('FIREBASE_DATABASE_URL', '').rstrip('/')\n"
191
+ "LOCAL_BACKUP = os.path.join(os.path.dirname(__file__), 'data', 'local_coap_env.json')\n\n"
192
+ "def save_to_supabase(record: dict) -> bool:\n"
193
+ " if not SUPABASE_URL or not SUPABASE_KEY or 'your-project' in SUPABASE_URL:\n"
194
+ " return False\n"
195
+ " headers = {'apikey': SUPABASE_KEY, 'Authorization': f'Bearer {SUPABASE_KEY}', 'Content-Type': 'application/json'}\n"
196
+ " resp = requests.post(f'{SUPABASE_URL}/rest/v1/coap_environmental_records', json=record, headers=headers, timeout=5)\n"
197
+ " return resp.status_code in (200, 201, 204)\n\n"
198
+ "def save_to_firebase(record: dict) -> bool:\n"
199
+ " if not FIREBASE_URL or 'your-firebase' in FIREBASE_URL:\n"
200
+ " return False\n"
201
+ " resp = requests.post(f'{FIREBASE_URL}/coap_environmental_records.json', json=record, timeout=5)\n"
202
+ " return resp.status_code == 200\n\n"
203
+ "def save_locally(record: dict):\n"
204
+ " os.makedirs(os.path.dirname(LOCAL_BACKUP), exist_ok=True)\n"
205
+ " records = []\n"
206
+ " if os.path.exists(LOCAL_BACKUP):\n"
207
+ " try:\n"
208
+ " with open(LOCAL_BACKUP, 'r', encoding='utf-8') as f:\n"
209
+ " records = json.load(f)\n"
210
+ " except Exception:\n"
211
+ " records = []\n"
212
+ " records.append(record)\n"
213
+ " with open(LOCAL_BACKUP, 'w', encoding='utf-8') as f:\n"
214
+ " json.dump(records, f, indent=2)\n\n"
215
+ "def main():\n"
216
+ " parser = argparse.ArgumentParser(description='CoAP Environmental Client')\n"
217
+ " parser.add_argument('--host', default=COAP_HOST, help='CoAP server host')\n"
218
+ " parser.add_argument('--port', type=int, default=COAP_PORT, help='CoAP server port')\n"
219
+ " args = parser.parse_args()\n\n"
220
+ " client = CoAPClient(timeout=6.0)\n"
221
+ " print(f\"Sending CoAP GET to coap://{args.host}:{args.port}/environmental/live...\")\n\n"
222
+ " try:\n"
223
+ " resp = client.get(args.host, args.port, '/environmental/live')\n"
224
+ " data = json.loads(resp.payload_text)\n"
225
+ " data['created_at'] = datetime.now(timezone.utc).isoformat()\n\n"
226
+ " print('✓ Received CoAP Response [2.05 Content]:')\n"
227
+ " print(f\" Coordinates : Lat={data.get('latitude')}, Lon={data.get('longitude')}\")\n"
228
+ " print(f\" Temperature : {data.get('temperature')} °C\")\n"
229
+ " print(f\" Humidity : {data.get('humidity')} %\")\n"
230
+ " print(f\" PM2.5 : {data.get('pm2_5')} μg/m³\")\n"
231
+ " print(f\" PM10 : {data.get('pm10')} μg/m³\")\n"
232
+ " print(f\" CO : {data.get('carbon_monoxide')} μg/m³\")\n"
233
+ " print(f\" NO2 : {data.get('nitrogen_dioxide')} μg/m³\")\n"
234
+ " print(f\" O3 : {data.get('ozone')} μg/m³\")\n"
235
+ " print(f\" European AQI : {data.get('european_aqi')}\")\n\n"
236
+ " cloud_ok = False\n"
237
+ " if CLOUD_PROVIDER == 'supabase':\n"
238
+ " cloud_ok = save_to_supabase(data)\n"
239
+ " elif CLOUD_PROVIDER == 'firebase':\n"
240
+ " cloud_ok = save_to_firebase(data)\n"
241
+ " save_locally(data)\n\n"
242
+ " status_msg = 'Stored in cloud database' if cloud_ok else 'Cached locally (cloud keys pending)'\n"
243
+ " print(f\"✓ Data Persistence: {status_msg}\")\n"
244
+ " except Exception as err:\n"
245
+ " print(f\"✗ CoAP Request failed: {err}\")\n"
246
+ " print(' Ensure `python coap_env_server.py` is actively running.')\n\n"
247
+ "if __name__ == '__main__':\n"
248
+ " main()\n"
249
+ )
250
+
251
+ # dashboard.py
252
+ files["dashboard.py"] = (
253
+ "import os\n"
254
+ "import json\n"
255
+ "import requests\n"
256
+ "import streamlit as st\n"
257
+ "from dotenv import load_dotenv\n\n"
258
+ "load_dotenv()\n\n"
259
+ "st.set_page_config(page_title='Program 10: CoAP Environmental Dashboard', layout='wide')\n"
260
+ "st.title('🌿 CoAP Environmental Monitoring Dashboard')\n"
261
+ "st.caption('MSc/MCA IoT Practical — Program 10 (Open-Meteo + CoAP + Cloud DB)')\n\n"
262
+ "CLOUD_PROVIDER = os.getenv('CLOUD_PROVIDER', 'supabase').lower().strip()\n"
263
+ "SUPABASE_URL = os.getenv('SUPABASE_URL', '').rstrip('/')\n"
264
+ "SUPABASE_KEY = os.getenv('SUPABASE_KEY', '')\n"
265
+ "LOCAL_BACKUP = os.path.join(os.path.dirname(__file__), 'data', 'local_coap_env.json')\n\n"
266
+ "def fetch_records():\n"
267
+ " if CLOUD_PROVIDER == 'supabase' and SUPABASE_URL and 'your-project' not in SUPABASE_URL:\n"
268
+ " try:\n"
269
+ " headers = {'apikey': SUPABASE_KEY, 'Authorization': f'Bearer {SUPABASE_KEY}'}\n"
270
+ " resp = requests.get(f'{SUPABASE_URL}/rest/v1/coap_environmental_records?order=created_at.desc&limit=30', headers=headers, timeout=4)\n"
271
+ " if resp.status_code == 200:\n"
272
+ " return resp.json()\n"
273
+ " except Exception:\n"
274
+ " pass\n"
275
+ " if os.path.exists(LOCAL_BACKUP):\n"
276
+ " try:\n"
277
+ " with open(LOCAL_BACKUP, 'r', encoding='utf-8') as f:\n"
278
+ " return list(reversed(json.load(f)))\n"
279
+ " except Exception:\n"
280
+ " pass\n"
281
+ " return []\n\n"
282
+ "records = fetch_records()\n"
283
+ "if not records:\n"
284
+ " st.warning('No records found. Run `python coap_env_server.py` in Terminal 1 and `python coap_env_client.py` in Terminal 2.')\n"
285
+ "else:\n"
286
+ " curr = records[0]\n"
287
+ " c1, c2, c3, c4 = st.columns(4)\n"
288
+ " c1.metric('Temperature', f\"{curr.get('temperature', 'N/A')} °C\")\n"
289
+ " c2.metric('Humidity', f\"{curr.get('humidity', 'N/A')} %\")\n"
290
+ " c3.metric('PM2.5 Pollutant', f\"{curr.get('pm2_5', 'N/A')} μg/m³\")\n"
291
+ " c4.metric('European AQI', str(curr.get('european_aqi', 'N/A')))\n\n"
292
+ " st.subheader('📊 Environmental & Air Quality Data Log')\n"
293
+ " st.dataframe(records, use_container_width=True)\n"
294
+ )
295
+
296
+ # README.md
297
+ if mode == "learning":
298
+ readme_content = (
299
+ "# Program 10 — CoAP Environmental Monitoring\n\n"
300
+ "## 1. Overview\n"
301
+ "This practical integrates live external APIs with the Constrained Application Protocol (CoAP):\n"
302
+ "- Queries **Open-Meteo Weather API** and **Open-Meteo Air Quality API** for configurable coordinates (Kalamassery/Kochi: `10.0159°N, 76.3419°E`).\n"
303
+ "- A CoAP Server exposes the live combined data over UDP at `coap://localhost:56830/environmental/live`.\n"
304
+ "- A CoAP Client retrieves the readings via UDP GET requests and persists them into Supabase or Firebase.\n"
305
+ "- A Streamlit web dashboard visualizes weather parameters, pollutant concentrations, and AQI.\n\n"
306
+ "## 2. Execution Guide\n\n"
307
+ "### Terminal 1 — Start the CoAP Environmental Server\n"
308
+ "```bash\n"
309
+ "python coap_env_server.py\n"
310
+ "```\n\n"
311
+ "### Terminal 2 — Query via CoAP Client & Store to DB\n"
312
+ "```bash\n"
313
+ "python coap_env_client.py\n"
314
+ "```\n\n"
315
+ "### Terminal 3 — Launch Dashboard\n"
316
+ "```bash\n"
317
+ "streamlit run dashboard.py\n"
318
+ "```\n\n"
319
+ "## 3. Viva Questions and Answers\n"
320
+ "**Q1: Why is an API bridge used between Open-Meteo and CoAP?** \n"
321
+ "A: Public cloud APIs provide HTTP REST interfaces. In constrained IoT networks, edge gateways query HTTP APIs, compress the telemetry into lightweight packets, and distribute it internally via CoAP UDP to power-limited sensor nodes.\n\n"
322
+ "**Q2: What is European AQI (EAQI)?** \n"
323
+ "A: EAQI is an index developed by the European Environment Agency based on hourly concentrations of 5 key pollutants: PM2.5, PM10, NO2, O3, and SO2.\n"
324
+ )
325
+ else:
326
+ readme_content = (
327
+ "# Program 10 — CoAP Environmental Monitoring (Exam Mode)\n\n"
328
+ "## Execution\n"
329
+ "1. Terminal 1: `python coap_env_server.py`\n"
330
+ "2. Terminal 2: `python coap_env_client.py`\n"
331
+ "3. Terminal 3: `streamlit run dashboard.py`\n\n"
332
+ "## Viva Q&A\n"
333
+ "- **Q: What data does Open-Meteo Air Quality API provide?** \n"
334
+ " A: Hourly concentrations of PM10, PM2.5, Carbon Monoxide, Nitrogen Dioxide, Ozone, and European AQI.\n"
335
+ )
336
+
337
+ files["README.md"] = readme_content
338
+ return files