iotlabkit 1.0.2__tar.gz → 1.0.3__tar.gz
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.
- {iotlabkit-1.0.2/src/iotlabkit.egg-info → iotlabkit-1.0.3}/PKG-INFO +1 -1
- {iotlabkit-1.0.2 → iotlabkit-1.0.3}/pyproject.toml +1 -1
- {iotlabkit-1.0.2 → iotlabkit-1.0.3}/src/iotlabkit/__init__.py +1 -1
- {iotlabkit-1.0.2 → iotlabkit-1.0.3}/src/iotlabkit/templates/program_05.py +49 -81
- {iotlabkit-1.0.2 → iotlabkit-1.0.3}/src/iotlabkit/templates/program_06.py +36 -79
- {iotlabkit-1.0.2 → iotlabkit-1.0.3}/src/iotlabkit/templates/program_10.py +47 -86
- {iotlabkit-1.0.2 → iotlabkit-1.0.3/src/iotlabkit.egg-info}/PKG-INFO +1 -1
- {iotlabkit-1.0.2 → iotlabkit-1.0.3}/LICENSE +0 -0
- {iotlabkit-1.0.2 → iotlabkit-1.0.3}/README.md +0 -0
- {iotlabkit-1.0.2 → iotlabkit-1.0.3}/setup.cfg +0 -0
- {iotlabkit-1.0.2 → iotlabkit-1.0.3}/src/iotlabkit/cli.py +0 -0
- {iotlabkit-1.0.2 → iotlabkit-1.0.3}/src/iotlabkit/coap_util.py +0 -0
- {iotlabkit-1.0.2 → iotlabkit-1.0.3}/src/iotlabkit/data/beijing_air/beijing_sample.csv +0 -0
- {iotlabkit-1.0.2 → iotlabkit-1.0.3}/src/iotlabkit/data/india_air/city_day_sample.csv +0 -0
- {iotlabkit-1.0.2 → iotlabkit-1.0.3}/src/iotlabkit/data/occupancy/datatraining.txt +0 -0
- {iotlabkit-1.0.2 → iotlabkit-1.0.3}/src/iotlabkit/doctor.py +0 -0
- {iotlabkit-1.0.2 → iotlabkit-1.0.3}/src/iotlabkit/generator.py +0 -0
- {iotlabkit-1.0.2 → iotlabkit-1.0.3}/src/iotlabkit/registry.py +0 -0
- {iotlabkit-1.0.2 → iotlabkit-1.0.3}/src/iotlabkit/templates/program_07.py +0 -0
- {iotlabkit-1.0.2 → iotlabkit-1.0.3}/src/iotlabkit/templates/program_08.py +0 -0
- {iotlabkit-1.0.2 → iotlabkit-1.0.3}/src/iotlabkit/templates/program_09.py +0 -0
- {iotlabkit-1.0.2 → iotlabkit-1.0.3}/src/iotlabkit/templates/program_11.py +0 -0
- {iotlabkit-1.0.2 → iotlabkit-1.0.3}/src/iotlabkit/templates/program_12.py +0 -0
- {iotlabkit-1.0.2 → iotlabkit-1.0.3}/src/iotlabkit/templates/program_13.py +0 -0
- {iotlabkit-1.0.2 → iotlabkit-1.0.3}/src/iotlabkit/templates/program_14.py +0 -0
- {iotlabkit-1.0.2 → iotlabkit-1.0.3}/src/iotlabkit/templates/program_15.py +0 -0
- {iotlabkit-1.0.2 → iotlabkit-1.0.3}/src/iotlabkit/templates/program_16.py +0 -0
- {iotlabkit-1.0.2 → iotlabkit-1.0.3}/src/iotlabkit/templates/program_17.py +0 -0
- {iotlabkit-1.0.2 → iotlabkit-1.0.3}/src/iotlabkit.egg-info/SOURCES.txt +0 -0
- {iotlabkit-1.0.2 → iotlabkit-1.0.3}/src/iotlabkit.egg-info/dependency_links.txt +0 -0
- {iotlabkit-1.0.2 → iotlabkit-1.0.3}/src/iotlabkit.egg-info/entry_points.txt +0 -0
- {iotlabkit-1.0.2 → iotlabkit-1.0.3}/src/iotlabkit.egg-info/requires.txt +0 -0
- {iotlabkit-1.0.2 → iotlabkit-1.0.3}/src/iotlabkit.egg-info/top_level.txt +0 -0
- {iotlabkit-1.0.2 → iotlabkit-1.0.3}/tests/test_api_parsing.py +0 -0
- {iotlabkit-1.0.2 → iotlabkit-1.0.3}/tests/test_cli.py +0 -0
- {iotlabkit-1.0.2 → iotlabkit-1.0.3}/tests/test_coap_util.py +0 -0
- {iotlabkit-1.0.2 → iotlabkit-1.0.3}/tests/test_doctor.py +0 -0
- {iotlabkit-1.0.2 → iotlabkit-1.0.3}/tests/test_generator.py +0 -0
- {iotlabkit-1.0.2 → iotlabkit-1.0.3}/tests/test_ml_pipeline.py +0 -0
- {iotlabkit-1.0.2 → iotlabkit-1.0.3}/tests/test_registry.py +0 -0
|
@@ -4,7 +4,7 @@ Designed for MSc/MCA Semester 3 IoT Laboratory practicals (Programs 5-17).
|
|
|
4
4
|
Generates clean, working, self-contained project templates in Learning and Exam modes.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
__version__ = "1.0.
|
|
7
|
+
__version__ = "1.0.3"
|
|
8
8
|
__author__ = "IoT Lab Contributors"
|
|
9
9
|
__license__ = "MIT"
|
|
10
10
|
|
|
@@ -11,17 +11,15 @@ def generate(target_dir: str, mode: str = "learning") -> Dict[str, str]:
|
|
|
11
11
|
"python-dotenv>=1.0.0\n"
|
|
12
12
|
"flask>=2.2.0\n"
|
|
13
13
|
"streamlit>=1.25.0\n"
|
|
14
|
-
"psycopg2-binary>=2.9.0\n"
|
|
15
14
|
)
|
|
16
15
|
|
|
17
16
|
files[".env.example"] = (
|
|
18
17
|
"PORT=5000\n"
|
|
19
18
|
"HOST=127.0.0.1\n"
|
|
20
|
-
"CLOUD_PROVIDER=
|
|
19
|
+
"CLOUD_PROVIDER=firebase\n"
|
|
20
|
+
"FIREBASE_DATABASE_URL=https://your-project.firebaseio.com\n"
|
|
21
21
|
"SUPABASE_URL=https://your-project.supabase.co\n"
|
|
22
22
|
"SUPABASE_KEY=your-supabase-key\n"
|
|
23
|
-
"SUPABASE_DB_PASSWORD=your-database-password\n"
|
|
24
|
-
"FIREBASE_DATABASE_URL=https://your-project.firebaseio.com\n"
|
|
25
23
|
)
|
|
26
24
|
|
|
27
25
|
files[".gitignore"] = (
|
|
@@ -45,9 +43,10 @@ def generate(target_dir: str, mode: str = "learning") -> Dict[str, str]:
|
|
|
45
43
|
"# Firebase Setup Guide\n\n"
|
|
46
44
|
"1. Open https://console.firebase.google.com/ and create a project.\n"
|
|
47
45
|
"2. Go to Build -> Realtime Database -> Create Database.\n"
|
|
48
|
-
"3. Select test mode rules.\n"
|
|
49
|
-
"4. Copy database URL
|
|
50
|
-
"5.
|
|
46
|
+
"3. Select 'Start in test mode' rules.\n"
|
|
47
|
+
"4. Copy the database URL (e.g. https://your-project-rtdb.firebaseio.com/).\n"
|
|
48
|
+
"5. Paste the URL into .env under FIREBASE_DATABASE_URL.\n"
|
|
49
|
+
"6. Tables/paths are created automatically on first insert (zero SQL needed).\n"
|
|
51
50
|
)
|
|
52
51
|
|
|
53
52
|
# server.py
|
|
@@ -62,22 +61,28 @@ def generate(target_dir: str, mode: str = "learning") -> Dict[str, str]:
|
|
|
62
61
|
"app = Flask(__name__)\n\n"
|
|
63
62
|
"HOST = os.getenv('HOST', '127.0.0.1')\n"
|
|
64
63
|
"PORT = int(os.getenv('PORT', 5000))\n"
|
|
65
|
-
"CLOUD_PROVIDER = os.getenv('CLOUD_PROVIDER', '
|
|
64
|
+
"CLOUD_PROVIDER = os.getenv('CLOUD_PROVIDER', 'firebase').lower().strip()\n"
|
|
65
|
+
"FIREBASE_URL = os.getenv('FIREBASE_DATABASE_URL', '').rstrip('/')\n"
|
|
66
66
|
"SUPABASE_URL = os.getenv('SUPABASE_URL', '').rstrip('/')\n"
|
|
67
67
|
"SUPABASE_KEY = os.getenv('SUPABASE_KEY', '')\n"
|
|
68
|
-
"FIREBASE_URL = os.getenv('FIREBASE_DATABASE_URL', '').rstrip('/')\n"
|
|
69
68
|
"LOCAL_BACKUP = os.path.join(os.path.dirname(__file__), 'data', 'local_telemetry.json')\n\n"
|
|
69
|
+
"def save_to_firebase(record):\n"
|
|
70
|
+
" if not FIREBASE_URL or 'your-project' in FIREBASE_URL:\n"
|
|
71
|
+
" return False\n"
|
|
72
|
+
" try:\n"
|
|
73
|
+
" resp = requests.post(f'{FIREBASE_URL}/sensor_telemetry.json', json=record, timeout=5)\n"
|
|
74
|
+
" return resp.status_code == 200\n"
|
|
75
|
+
" except Exception:\n"
|
|
76
|
+
" return False\n\n"
|
|
70
77
|
"def save_to_supabase(record):\n"
|
|
71
78
|
" if not SUPABASE_URL or not SUPABASE_KEY or 'your-project' in SUPABASE_URL:\n"
|
|
72
79
|
" return False\n"
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
"
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
" return False\n"
|
|
79
|
-
" resp = requests.post(f'{FIREBASE_URL}/sensor_telemetry.json', json=record, timeout=5)\n"
|
|
80
|
-
" return resp.status_code == 200\n\n"
|
|
80
|
+
" try:\n"
|
|
81
|
+
" headers = {'apikey': SUPABASE_KEY, 'Authorization': f'Bearer {SUPABASE_KEY}', 'Content-Type': 'application/json'}\n"
|
|
82
|
+
" resp = requests.post(f'{SUPABASE_URL}/rest/v1/sensor_telemetry', json=record, headers=headers, timeout=5)\n"
|
|
83
|
+
" return resp.status_code in (200, 201, 204)\n"
|
|
84
|
+
" except Exception:\n"
|
|
85
|
+
" return False\n\n"
|
|
81
86
|
"def save_local(record):\n"
|
|
82
87
|
" os.makedirs(os.path.dirname(LOCAL_BACKUP), exist_ok=True)\n"
|
|
83
88
|
" data = []\n"
|
|
@@ -102,95 +107,58 @@ def generate(target_dir: str, mode: str = "learning") -> Dict[str, str]:
|
|
|
102
107
|
" 'created_at': datetime.now(timezone.utc).isoformat()\n"
|
|
103
108
|
" }\n"
|
|
104
109
|
" saved_cloud = False\n"
|
|
105
|
-
" if CLOUD_PROVIDER == '
|
|
106
|
-
" saved_cloud = save_to_supabase(record)\n"
|
|
107
|
-
" elif CLOUD_PROVIDER == 'firebase':\n"
|
|
110
|
+
" if CLOUD_PROVIDER == 'firebase':\n"
|
|
108
111
|
" saved_cloud = save_to_firebase(record)\n"
|
|
112
|
+
" elif CLOUD_PROVIDER == 'supabase':\n"
|
|
113
|
+
" saved_cloud = save_to_supabase(record)\n"
|
|
109
114
|
" save_local(record)\n"
|
|
110
|
-
" status = 'Cloud
|
|
115
|
+
" status = 'Stored in Cloud' if saved_cloud else 'Saved Locally'\n"
|
|
111
116
|
" print(f\"[{datetime.now().strftime('%H:%M:%S')}] Telemetry: Temp={temp}C, Humidity={hum}% -> {status}\")\n"
|
|
112
117
|
" return jsonify({'message': 'Accepted', 'cloud_synced': saved_cloud, 'record': record}), 201\n\n"
|
|
113
118
|
"@app.route('/api/telemetry', methods=['GET'])\n"
|
|
114
119
|
"def get_telemetry():\n"
|
|
120
|
+
" if CLOUD_PROVIDER == 'firebase' and FIREBASE_URL and 'your-project' not in FIREBASE_URL:\n"
|
|
121
|
+
" try:\n"
|
|
122
|
+
" resp = requests.get(f'{FIREBASE_URL}/sensor_telemetry.json', timeout=4)\n"
|
|
123
|
+
" if resp.status_code == 200 and resp.json():\n"
|
|
124
|
+
" data = resp.json()\n"
|
|
125
|
+
" if isinstance(data, dict):\n"
|
|
126
|
+
" return jsonify(list(reversed(list(data.values())))), 200\n"
|
|
127
|
+
" except Exception:\n"
|
|
128
|
+
" pass\n"
|
|
115
129
|
" if os.path.exists(LOCAL_BACKUP):\n"
|
|
116
130
|
" with open(LOCAL_BACKUP, 'r', encoding='utf-8') as f:\n"
|
|
117
131
|
" data = json.load(f)\n"
|
|
118
132
|
" return jsonify(list(reversed(data[-50:]))), 200\n"
|
|
119
133
|
" return jsonify([]), 200\n\n"
|
|
120
134
|
"if __name__ == '__main__':\n"
|
|
121
|
-
" try:\n"
|
|
122
|
-
" import init_db\n"
|
|
123
|
-
" init_db.init_table()\n"
|
|
124
|
-
" except Exception:\n"
|
|
125
|
-
" pass\n"
|
|
126
135
|
" print(f'Starting REST API server on http://{HOST}:{PORT}')\n"
|
|
127
136
|
" app.run(host=HOST, port=PORT, debug=False)\n"
|
|
128
137
|
)
|
|
129
138
|
|
|
130
139
|
files["init_db.py"] = (
|
|
131
140
|
"import os\n"
|
|
132
|
-
"import re\n"
|
|
133
141
|
"import requests\n"
|
|
134
142
|
"from dotenv import load_dotenv\n\n"
|
|
135
143
|
"load_dotenv()\n\n"
|
|
136
|
-
"
|
|
137
|
-
"
|
|
138
|
-
"
|
|
139
|
-
"
|
|
140
|
-
"
|
|
141
|
-
"SQL_QUERY = '''\n"
|
|
142
|
-
"CREATE TABLE IF NOT EXISTS sensor_telemetry (\n"
|
|
143
|
-
" id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,\n"
|
|
144
|
-
" created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::text, NOW()) NOT NULL,\n"
|
|
145
|
-
" sensor_id TEXT NOT NULL DEFAULT 'DHT22_01',\n"
|
|
146
|
-
" temperature DOUBLE PRECISION NOT NULL,\n"
|
|
147
|
-
" humidity DOUBLE PRECISION NOT NULL\n"
|
|
148
|
-
");\n"
|
|
149
|
-
"ALTER TABLE sensor_telemetry ENABLE ROW LEVEL SECURITY;\n"
|
|
150
|
-
"CREATE POLICY \"Allow anon read\" ON sensor_telemetry FOR SELECT USING (true);\n"
|
|
151
|
-
"CREATE POLICY \"Allow anon insert\" ON sensor_telemetry FOR INSERT WITH CHECK (true);\n"
|
|
152
|
-
"'''\n\n"
|
|
153
|
-
"def init_table():\n"
|
|
154
|
-
" if not SUPABASE_URL or 'your-project' in SUPABASE_URL:\n"
|
|
155
|
-
" return False\n"
|
|
156
|
-
" headers = {'apikey': SUPABASE_KEY, 'Authorization': f'Bearer {SUPABASE_KEY}'}\n"
|
|
157
|
-
" try:\n"
|
|
158
|
-
" r = requests.get(f'{SUPABASE_URL}/rest/v1/sensor_telemetry?limit=1', headers=headers, timeout=4)\n"
|
|
159
|
-
" if r.status_code == 200:\n"
|
|
160
|
-
" print('Supabase: Table sensor_telemetry is ready.')\n"
|
|
161
|
-
" return True\n"
|
|
162
|
-
" except Exception:\n"
|
|
163
|
-
" pass\n"
|
|
164
|
-
" m = re.search(r'https://([a-z0-9_-]+)\\.supabase\\.co', SUPABASE_URL)\n"
|
|
165
|
-
" ref = m.group(1) if m else None\n"
|
|
166
|
-
" if ACCESS_TOKEN and ref:\n"
|
|
144
|
+
"FIREBASE_URL = os.getenv('FIREBASE_DATABASE_URL', '').rstrip('/')\n"
|
|
145
|
+
"SUPABASE_URL = os.getenv('SUPABASE_URL', '').rstrip('/')\n\n"
|
|
146
|
+
"def check():\n"
|
|
147
|
+
" print('Database Check:')\n"
|
|
148
|
+
" if FIREBASE_URL and 'your-project' not in FIREBASE_URL:\n"
|
|
167
149
|
" try:\n"
|
|
168
|
-
"
|
|
169
|
-
" if
|
|
170
|
-
" print('
|
|
150
|
+
" r = requests.get(f'{FIREBASE_URL}/.json?shallow=true', timeout=5)\n"
|
|
151
|
+
" if r.status_code == 200:\n"
|
|
152
|
+
" print('Firebase: Connected. Tables are created automatically on first insert.')\n"
|
|
171
153
|
" return True\n"
|
|
172
|
-
" except Exception:\n"
|
|
173
|
-
" pass\n"
|
|
174
|
-
" conn_str = DATABASE_URL\n"
|
|
175
|
-
" if not conn_str and DB_PASSWORD and ref:\n"
|
|
176
|
-
" conn_str = f'postgresql://postgres:{DB_PASSWORD}@db.{ref}.supabase.co:5432/postgres'\n"
|
|
177
|
-
" if conn_str:\n"
|
|
178
|
-
" try:\n"
|
|
179
|
-
" import psycopg2\n"
|
|
180
|
-
" conn = psycopg2.connect(conn_str, connect_timeout=6)\n"
|
|
181
|
-
" cur = conn.cursor()\n"
|
|
182
|
-
" cur.execute(SQL_QUERY)\n"
|
|
183
|
-
" conn.commit()\n"
|
|
184
|
-
" cur.close()\n"
|
|
185
|
-
" conn.close()\n"
|
|
186
|
-
" print('Supabase: Table sensor_telemetry created via PostgreSQL.')\n"
|
|
187
|
-
" return True\n"
|
|
188
154
|
" except Exception as e:\n"
|
|
189
|
-
" print(
|
|
190
|
-
"
|
|
191
|
-
"
|
|
155
|
+
" print('Firebase note:', e)\n"
|
|
156
|
+
" if SUPABASE_URL and 'your-project' not in SUPABASE_URL:\n"
|
|
157
|
+
" print('Supabase configured. Schema is in supabase_schema.sql.')\n"
|
|
158
|
+
" print('Local database: Ready (data/local_telemetry.json)')\n"
|
|
159
|
+
" return True\n\n"
|
|
192
160
|
"if __name__ == '__main__':\n"
|
|
193
|
-
"
|
|
161
|
+
" check()\n"
|
|
194
162
|
)
|
|
195
163
|
|
|
196
164
|
# client_sender.py
|
|
@@ -11,18 +11,16 @@ def generate(target_dir: str, mode: str = "learning") -> Dict[str, str]:
|
|
|
11
11
|
"requests>=2.28.0\n"
|
|
12
12
|
"python-dotenv>=1.0.0\n"
|
|
13
13
|
"streamlit>=1.25.0\n"
|
|
14
|
-
"psycopg2-binary>=2.9.0\n"
|
|
15
14
|
)
|
|
16
15
|
|
|
17
16
|
files[".env.example"] = (
|
|
18
17
|
"MQTT_BROKER=broker.hivemq.com\n"
|
|
19
18
|
"MQTT_PORT=1883\n"
|
|
20
19
|
"MQTT_TOPIC=iot/lab/telemetry\n"
|
|
21
|
-
"CLOUD_PROVIDER=
|
|
20
|
+
"CLOUD_PROVIDER=firebase\n"
|
|
21
|
+
"FIREBASE_DATABASE_URL=https://your-project.firebaseio.com\n"
|
|
22
22
|
"SUPABASE_URL=https://your-project.supabase.co\n"
|
|
23
23
|
"SUPABASE_KEY=your-supabase-key\n"
|
|
24
|
-
"SUPABASE_DB_PASSWORD=your-database-password\n"
|
|
25
|
-
"FIREBASE_DATABASE_URL=https://your-project.firebaseio.com\n"
|
|
26
24
|
)
|
|
27
25
|
|
|
28
26
|
files[".gitignore"] = (
|
|
@@ -57,22 +55,28 @@ def generate(target_dir: str, mode: str = "learning") -> Dict[str, str]:
|
|
|
57
55
|
"BROKER = os.getenv('MQTT_BROKER', 'broker.hivemq.com')\n"
|
|
58
56
|
"PORT = int(os.getenv('MQTT_PORT', 1883))\n"
|
|
59
57
|
"TOPIC = os.getenv('MQTT_TOPIC', 'iot/lab/telemetry')\n"
|
|
60
|
-
"CLOUD_PROVIDER = os.getenv('CLOUD_PROVIDER', '
|
|
58
|
+
"CLOUD_PROVIDER = os.getenv('CLOUD_PROVIDER', 'firebase').lower().strip()\n"
|
|
59
|
+
"FIREBASE_URL = os.getenv('FIREBASE_DATABASE_URL', '').rstrip('/')\n"
|
|
61
60
|
"SUPABASE_URL = os.getenv('SUPABASE_URL', '').rstrip('/')\n"
|
|
62
61
|
"SUPABASE_KEY = os.getenv('SUPABASE_KEY', '')\n"
|
|
63
|
-
"FIREBASE_URL = os.getenv('FIREBASE_DATABASE_URL', '').rstrip('/')\n"
|
|
64
62
|
"LOCAL_BACKUP = os.path.join(os.path.dirname(__file__), 'data', 'local_mqtt_telemetry.json')\n\n"
|
|
65
|
-
"def save_to_supabase(record):\n"
|
|
66
|
-
" if not SUPABASE_URL or not SUPABASE_KEY or 'your-project' in SUPABASE_URL:\n"
|
|
67
|
-
" return False\n"
|
|
68
|
-
" headers = {'apikey': SUPABASE_KEY, 'Authorization': f'Bearer {SUPABASE_KEY}', 'Content-Type': 'application/json'}\n"
|
|
69
|
-
" resp = requests.post(f'{SUPABASE_URL}/rest/v1/mqtt_telemetry', json=record, headers=headers, timeout=5)\n"
|
|
70
|
-
" return resp.status_code in (200, 201, 204)\n\n"
|
|
71
63
|
"def save_to_firebase(record):\n"
|
|
72
64
|
" if not FIREBASE_URL or 'your-firebase' in FIREBASE_URL:\n"
|
|
73
65
|
" return False\n"
|
|
74
|
-
"
|
|
75
|
-
"
|
|
66
|
+
" try:\n"
|
|
67
|
+
" resp = requests.post(f'{FIREBASE_URL}/mqtt_telemetry.json', json=record, timeout=5)\n"
|
|
68
|
+
" return resp.status_code == 200\n"
|
|
69
|
+
" except Exception:\n"
|
|
70
|
+
" return False\n\n"
|
|
71
|
+
"def save_to_supabase(record):\n"
|
|
72
|
+
" if not SUPABASE_URL or not SUPABASE_KEY or 'your-project' in SUPABASE_URL:\n"
|
|
73
|
+
" return False\n"
|
|
74
|
+
" try:\n"
|
|
75
|
+
" headers = {'apikey': SUPABASE_KEY, 'Authorization': f'Bearer {SUPABASE_KEY}', 'Content-Type': 'application/json'}\n"
|
|
76
|
+
" resp = requests.post(f'{SUPABASE_URL}/rest/v1/mqtt_telemetry', json=record, headers=headers, timeout=5)\n"
|
|
77
|
+
" return resp.status_code in (200, 201, 204)\n"
|
|
78
|
+
" except Exception:\n"
|
|
79
|
+
" return False\n\n"
|
|
76
80
|
"def save_local(record):\n"
|
|
77
81
|
" os.makedirs(os.path.dirname(LOCAL_BACKUP), exist_ok=True)\n"
|
|
78
82
|
" data = []\n"
|
|
@@ -105,21 +109,16 @@ def generate(target_dir: str, mode: str = "learning") -> Dict[str, str]:
|
|
|
105
109
|
" 'created_at': datetime.now(timezone.utc).isoformat()\n"
|
|
106
110
|
" }\n"
|
|
107
111
|
" saved = False\n"
|
|
108
|
-
" if CLOUD_PROVIDER == '
|
|
109
|
-
" saved = save_to_supabase(record)\n"
|
|
110
|
-
" elif CLOUD_PROVIDER == 'firebase':\n"
|
|
112
|
+
" if CLOUD_PROVIDER == 'firebase':\n"
|
|
111
113
|
" saved = save_to_firebase(record)\n"
|
|
114
|
+
" elif CLOUD_PROVIDER == 'supabase':\n"
|
|
115
|
+
" saved = save_to_supabase(record)\n"
|
|
112
116
|
" save_local(record)\n"
|
|
113
|
-
" status = 'Cloud
|
|
117
|
+
" status = 'Stored in Cloud' if saved else 'Saved Locally'\n"
|
|
114
118
|
" print(f\"[{datetime.now().strftime('%H:%M:%S')}] Received on {msg.topic}: Temp={temp}C, Hum={hum}% -> {status}\")\n"
|
|
115
119
|
" except Exception as e:\n"
|
|
116
120
|
" print('Message parse error:', e)\n\n"
|
|
117
121
|
"def main():\n"
|
|
118
|
-
" try:\n"
|
|
119
|
-
" import init_db\n"
|
|
120
|
-
" init_db.init_table()\n"
|
|
121
|
-
" except Exception:\n"
|
|
122
|
-
" pass\n"
|
|
123
122
|
" client = mqtt.Client()\n"
|
|
124
123
|
" client.on_connect = on_connect\n"
|
|
125
124
|
" client.on_message = on_message\n"
|
|
@@ -132,69 +131,27 @@ def generate(target_dir: str, mode: str = "learning") -> Dict[str, str]:
|
|
|
132
131
|
|
|
133
132
|
files["init_db.py"] = (
|
|
134
133
|
"import os\n"
|
|
135
|
-
"import re\n"
|
|
136
134
|
"import requests\n"
|
|
137
135
|
"from dotenv import load_dotenv\n\n"
|
|
138
136
|
"load_dotenv()\n\n"
|
|
139
|
-
"
|
|
140
|
-
"
|
|
141
|
-
"
|
|
142
|
-
"
|
|
143
|
-
"
|
|
144
|
-
"SQL_QUERY = '''\n"
|
|
145
|
-
"CREATE TABLE IF NOT EXISTS mqtt_telemetry (\n"
|
|
146
|
-
" id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,\n"
|
|
147
|
-
" created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::text, NOW()) NOT NULL,\n"
|
|
148
|
-
" topic TEXT NOT NULL,\n"
|
|
149
|
-
" sensor_id TEXT NOT NULL DEFAULT 'DHT22_01',\n"
|
|
150
|
-
" temperature DOUBLE PRECISION NOT NULL,\n"
|
|
151
|
-
" humidity DOUBLE PRECISION NOT NULL\n"
|
|
152
|
-
");\n"
|
|
153
|
-
"ALTER TABLE mqtt_telemetry ENABLE ROW LEVEL SECURITY;\n"
|
|
154
|
-
"CREATE POLICY \"Allow anon read\" ON mqtt_telemetry FOR SELECT USING (true);\n"
|
|
155
|
-
"CREATE POLICY \"Allow anon insert\" ON mqtt_telemetry FOR INSERT WITH CHECK (true);\n"
|
|
156
|
-
"'''\n\n"
|
|
157
|
-
"def init_table():\n"
|
|
158
|
-
" if not SUPABASE_URL or 'your-project' in SUPABASE_URL:\n"
|
|
159
|
-
" return False\n"
|
|
160
|
-
" headers = {'apikey': SUPABASE_KEY, 'Authorization': f'Bearer {SUPABASE_KEY}'}\n"
|
|
161
|
-
" try:\n"
|
|
162
|
-
" r = requests.get(f'{SUPABASE_URL}/rest/v1/mqtt_telemetry?limit=1', headers=headers, timeout=4)\n"
|
|
163
|
-
" if r.status_code == 200:\n"
|
|
164
|
-
" print('Supabase: Table mqtt_telemetry is ready.')\n"
|
|
165
|
-
" return True\n"
|
|
166
|
-
" except Exception:\n"
|
|
167
|
-
" pass\n"
|
|
168
|
-
" m = re.search(r'https://([a-z0-9_-]+)\\.supabase\\.co', SUPABASE_URL)\n"
|
|
169
|
-
" ref = m.group(1) if m else None\n"
|
|
170
|
-
" if ACCESS_TOKEN and ref:\n"
|
|
137
|
+
"FIREBASE_URL = os.getenv('FIREBASE_DATABASE_URL', '').rstrip('/')\n"
|
|
138
|
+
"SUPABASE_URL = os.getenv('SUPABASE_URL', '').rstrip('/')\n\n"
|
|
139
|
+
"def check():\n"
|
|
140
|
+
" print('Database Check:')\n"
|
|
141
|
+
" if FIREBASE_URL and 'your-project' not in FIREBASE_URL:\n"
|
|
171
142
|
" try:\n"
|
|
172
|
-
"
|
|
173
|
-
" if
|
|
174
|
-
" print('
|
|
143
|
+
" r = requests.get(f'{FIREBASE_URL}/.json?shallow=true', timeout=5)\n"
|
|
144
|
+
" if r.status_code == 200:\n"
|
|
145
|
+
" print('Firebase: Connected. Tables are created automatically on first insert.')\n"
|
|
175
146
|
" return True\n"
|
|
176
|
-
" except Exception:\n"
|
|
177
|
-
" pass\n"
|
|
178
|
-
" conn_str = DATABASE_URL\n"
|
|
179
|
-
" if not conn_str and DB_PASSWORD and ref:\n"
|
|
180
|
-
" conn_str = f'postgresql://postgres:{DB_PASSWORD}@db.{ref}.supabase.co:5432/postgres'\n"
|
|
181
|
-
" if conn_str:\n"
|
|
182
|
-
" try:\n"
|
|
183
|
-
" import psycopg2\n"
|
|
184
|
-
" conn = psycopg2.connect(conn_str, connect_timeout=6)\n"
|
|
185
|
-
" cur = conn.cursor()\n"
|
|
186
|
-
" cur.execute(SQL_QUERY)\n"
|
|
187
|
-
" conn.commit()\n"
|
|
188
|
-
" cur.close()\n"
|
|
189
|
-
" conn.close()\n"
|
|
190
|
-
" print('Supabase: Table mqtt_telemetry created via PostgreSQL.')\n"
|
|
191
|
-
" return True\n"
|
|
192
147
|
" except Exception as e:\n"
|
|
193
|
-
" print(
|
|
194
|
-
"
|
|
195
|
-
"
|
|
148
|
+
" print('Firebase note:', e)\n"
|
|
149
|
+
" if SUPABASE_URL and 'your-project' not in SUPABASE_URL:\n"
|
|
150
|
+
" print('Supabase configured. Schema is in supabase_schema.sql.')\n"
|
|
151
|
+
" print('Local database: Ready (data/local_mqtt_telemetry.json)')\n"
|
|
152
|
+
" return True\n\n"
|
|
196
153
|
"if __name__ == '__main__':\n"
|
|
197
|
-
"
|
|
154
|
+
" check()\n"
|
|
198
155
|
)
|
|
199
156
|
|
|
200
157
|
# publisher.py
|
|
@@ -11,7 +11,6 @@ def generate(target_dir: str, mode: str = "learning") -> Dict[str, str]:
|
|
|
11
11
|
"requests>=2.28.0\n"
|
|
12
12
|
"python-dotenv>=1.0.0\n"
|
|
13
13
|
"streamlit>=1.25.0\n"
|
|
14
|
-
"psycopg2-binary>=2.9.0\n"
|
|
15
14
|
)
|
|
16
15
|
|
|
17
16
|
files[".env.example"] = (
|
|
@@ -19,11 +18,10 @@ def generate(target_dir: str, mode: str = "learning") -> Dict[str, str]:
|
|
|
19
18
|
"LONGITUDE=76.3419\n"
|
|
20
19
|
"COAP_HOST=127.0.0.1\n"
|
|
21
20
|
"COAP_PORT=56830\n"
|
|
22
|
-
"CLOUD_PROVIDER=
|
|
21
|
+
"CLOUD_PROVIDER=firebase\n"
|
|
22
|
+
"FIREBASE_DATABASE_URL=https://your-firebase-project.firebaseio.com\n"
|
|
23
23
|
"SUPABASE_URL=https://your-project-ref.supabase.co\n"
|
|
24
24
|
"SUPABASE_KEY=your-supabase-anon-key\n"
|
|
25
|
-
"SUPABASE_DB_PASSWORD=your-database-password\n"
|
|
26
|
-
"FIREBASE_DATABASE_URL=https://your-firebase-project.firebaseio.com\n"
|
|
27
25
|
)
|
|
28
26
|
|
|
29
27
|
files[".gitignore"] = (
|
|
@@ -145,22 +143,28 @@ def generate(target_dir: str, mode: str = "learning") -> Dict[str, str]:
|
|
|
145
143
|
"load_dotenv()\n\n"
|
|
146
144
|
"COAP_HOST = os.getenv('COAP_HOST', '127.0.0.1')\n"
|
|
147
145
|
"COAP_PORT = int(os.getenv('COAP_PORT', 56830))\n"
|
|
148
|
-
"CLOUD_PROVIDER = os.getenv('CLOUD_PROVIDER', '
|
|
146
|
+
"CLOUD_PROVIDER = os.getenv('CLOUD_PROVIDER', 'firebase').lower().strip()\n"
|
|
147
|
+
"FIREBASE_URL = os.getenv('FIREBASE_DATABASE_URL', '').rstrip('/')\n"
|
|
149
148
|
"SUPABASE_URL = os.getenv('SUPABASE_URL', '').rstrip('/')\n"
|
|
150
149
|
"SUPABASE_KEY = os.getenv('SUPABASE_KEY', '')\n"
|
|
151
|
-
"FIREBASE_URL = os.getenv('FIREBASE_DATABASE_URL', '').rstrip('/')\n"
|
|
152
150
|
"LOCAL_BACKUP = os.path.join(os.path.dirname(__file__), 'data', 'local_coap_env.json')\n\n"
|
|
153
|
-
"def save_to_supabase(record):\n"
|
|
154
|
-
" if not SUPABASE_URL or not SUPABASE_KEY or 'your-project' in SUPABASE_URL:\n"
|
|
155
|
-
" return False\n"
|
|
156
|
-
" headers = {'apikey': SUPABASE_KEY, 'Authorization': f'Bearer {SUPABASE_KEY}', 'Content-Type': 'application/json'}\n"
|
|
157
|
-
" resp = requests.post(f'{SUPABASE_URL}/rest/v1/coap_environmental_records', json=record, headers=headers, timeout=5)\n"
|
|
158
|
-
" return resp.status_code in (200, 201, 204)\n\n"
|
|
159
151
|
"def save_to_firebase(record):\n"
|
|
160
152
|
" if not FIREBASE_URL or 'your-firebase' in FIREBASE_URL:\n"
|
|
161
153
|
" return False\n"
|
|
162
|
-
"
|
|
163
|
-
"
|
|
154
|
+
" try:\n"
|
|
155
|
+
" resp = requests.post(f'{FIREBASE_URL}/coap_environmental_records.json', json=record, timeout=5)\n"
|
|
156
|
+
" return resp.status_code == 200\n"
|
|
157
|
+
" except Exception:\n"
|
|
158
|
+
" return False\n\n"
|
|
159
|
+
"def save_to_supabase(record):\n"
|
|
160
|
+
" if not SUPABASE_URL or not SUPABASE_KEY or 'your-project' in SUPABASE_URL:\n"
|
|
161
|
+
" return False\n"
|
|
162
|
+
" try:\n"
|
|
163
|
+
" headers = {'apikey': SUPABASE_KEY, 'Authorization': f'Bearer {SUPABASE_KEY}', 'Content-Type': 'application/json'}\n"
|
|
164
|
+
" resp = requests.post(f'{SUPABASE_URL}/rest/v1/coap_environmental_records', json=record, headers=headers, timeout=5)\n"
|
|
165
|
+
" return resp.status_code in (200, 201, 204)\n"
|
|
166
|
+
" except Exception:\n"
|
|
167
|
+
" return False\n\n"
|
|
164
168
|
"def save_locally(record):\n"
|
|
165
169
|
" os.makedirs(os.path.dirname(LOCAL_BACKUP), exist_ok=True)\n"
|
|
166
170
|
" records = []\n"
|
|
@@ -174,11 +178,6 @@ def generate(target_dir: str, mode: str = "learning") -> Dict[str, str]:
|
|
|
174
178
|
" with open(LOCAL_BACKUP, 'w', encoding='utf-8') as f:\n"
|
|
175
179
|
" json.dump(records, f, indent=2)\n\n"
|
|
176
180
|
"def main():\n"
|
|
177
|
-
" try:\n"
|
|
178
|
-
" import init_db\n"
|
|
179
|
-
" init_db.init_table()\n"
|
|
180
|
-
" except Exception:\n"
|
|
181
|
-
" pass\n"
|
|
182
181
|
" parser = argparse.ArgumentParser()\n"
|
|
183
182
|
" parser.add_argument('--host', default=COAP_HOST)\n"
|
|
184
183
|
" parser.add_argument('--port', type=int, default=COAP_PORT)\n"
|
|
@@ -200,12 +199,12 @@ def generate(target_dir: str, mode: str = "learning") -> Dict[str, str]:
|
|
|
200
199
|
" print(f\" O3 : {data.get('ozone')} ug/m3\")\n"
|
|
201
200
|
" print(f\" European AQI : {data.get('european_aqi')}\")\n\n"
|
|
202
201
|
" cloud_ok = False\n"
|
|
203
|
-
" if CLOUD_PROVIDER == '
|
|
204
|
-
" cloud_ok = save_to_supabase(data)\n"
|
|
205
|
-
" elif CLOUD_PROVIDER == 'firebase':\n"
|
|
202
|
+
" if CLOUD_PROVIDER == 'firebase':\n"
|
|
206
203
|
" cloud_ok = save_to_firebase(data)\n"
|
|
204
|
+
" elif CLOUD_PROVIDER == 'supabase':\n"
|
|
205
|
+
" cloud_ok = save_to_supabase(data)\n"
|
|
207
206
|
" save_locally(data)\n\n"
|
|
208
|
-
" status_msg = 'Stored in
|
|
207
|
+
" status_msg = 'Stored in Cloud' if cloud_ok else 'Saved locally'\n"
|
|
209
208
|
" print(f\"Status: {status_msg}\")\n"
|
|
210
209
|
" except Exception as err:\n"
|
|
211
210
|
" print(f\"Request failed: {err}\")\n\n"
|
|
@@ -215,75 +214,27 @@ def generate(target_dir: str, mode: str = "learning") -> Dict[str, str]:
|
|
|
215
214
|
|
|
216
215
|
files["init_db.py"] = (
|
|
217
216
|
"import os\n"
|
|
218
|
-
"import re\n"
|
|
219
217
|
"import requests\n"
|
|
220
218
|
"from dotenv import load_dotenv\n\n"
|
|
221
219
|
"load_dotenv()\n\n"
|
|
222
|
-
"
|
|
223
|
-
"
|
|
224
|
-
"
|
|
225
|
-
"
|
|
226
|
-
"
|
|
227
|
-
"SQL_QUERY = '''\n"
|
|
228
|
-
"CREATE TABLE IF NOT EXISTS coap_environmental_records (\n"
|
|
229
|
-
" id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,\n"
|
|
230
|
-
" created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::text, NOW()) NOT NULL,\n"
|
|
231
|
-
" latitude DOUBLE PRECISION NOT NULL,\n"
|
|
232
|
-
" longitude DOUBLE PRECISION NOT NULL,\n"
|
|
233
|
-
" temperature DOUBLE PRECISION,\n"
|
|
234
|
-
" humidity DOUBLE PRECISION,\n"
|
|
235
|
-
" pm10 DOUBLE PRECISION,\n"
|
|
236
|
-
" pm2_5 DOUBLE PRECISION,\n"
|
|
237
|
-
" carbon_monoxide DOUBLE PRECISION,\n"
|
|
238
|
-
" nitrogen_dioxide DOUBLE PRECISION,\n"
|
|
239
|
-
" ozone DOUBLE PRECISION,\n"
|
|
240
|
-
" european_aqi INTEGER\n"
|
|
241
|
-
");\n"
|
|
242
|
-
"ALTER TABLE coap_environmental_records ENABLE ROW LEVEL SECURITY;\n"
|
|
243
|
-
"CREATE POLICY \"Allow anon read\" ON coap_environmental_records FOR SELECT USING (true);\n"
|
|
244
|
-
"CREATE POLICY \"Allow anon insert\" ON coap_environmental_records FOR INSERT WITH CHECK (true);\n"
|
|
245
|
-
"'''\n\n"
|
|
246
|
-
"def init_table():\n"
|
|
247
|
-
" if not SUPABASE_URL or 'your-project' in SUPABASE_URL:\n"
|
|
248
|
-
" return False\n"
|
|
249
|
-
" headers = {'apikey': SUPABASE_KEY, 'Authorization': f'Bearer {SUPABASE_KEY}'}\n"
|
|
250
|
-
" try:\n"
|
|
251
|
-
" r = requests.get(f'{SUPABASE_URL}/rest/v1/coap_environmental_records?limit=1', headers=headers, timeout=4)\n"
|
|
252
|
-
" if r.status_code == 200:\n"
|
|
253
|
-
" print('Supabase: Table coap_environmental_records is ready.')\n"
|
|
254
|
-
" return True\n"
|
|
255
|
-
" except Exception:\n"
|
|
256
|
-
" pass\n"
|
|
257
|
-
" m = re.search(r'https://([a-z0-9_-]+)\\.supabase\\.co', SUPABASE_URL)\n"
|
|
258
|
-
" ref = m.group(1) if m else None\n"
|
|
259
|
-
" if ACCESS_TOKEN and ref:\n"
|
|
220
|
+
"FIREBASE_URL = os.getenv('FIREBASE_DATABASE_URL', '').rstrip('/')\n"
|
|
221
|
+
"SUPABASE_URL = os.getenv('SUPABASE_URL', '').rstrip('/')\n\n"
|
|
222
|
+
"def check():\n"
|
|
223
|
+
" print('Database Check:')\n"
|
|
224
|
+
" if FIREBASE_URL and 'your-firebase' not in FIREBASE_URL:\n"
|
|
260
225
|
" try:\n"
|
|
261
|
-
"
|
|
262
|
-
" if
|
|
263
|
-
" print('
|
|
226
|
+
" r = requests.get(f'{FIREBASE_URL}/.json?shallow=true', timeout=5)\n"
|
|
227
|
+
" if r.status_code == 200:\n"
|
|
228
|
+
" print('Firebase: Connected. Tables are created automatically on first insert.')\n"
|
|
264
229
|
" return True\n"
|
|
265
|
-
" except Exception:\n"
|
|
266
|
-
" pass\n"
|
|
267
|
-
" conn_str = DATABASE_URL\n"
|
|
268
|
-
" if not conn_str and DB_PASSWORD and ref:\n"
|
|
269
|
-
" conn_str = f'postgresql://postgres:{DB_PASSWORD}@db.{ref}.supabase.co:5432/postgres'\n"
|
|
270
|
-
" if conn_str:\n"
|
|
271
|
-
" try:\n"
|
|
272
|
-
" import psycopg2\n"
|
|
273
|
-
" conn = psycopg2.connect(conn_str, connect_timeout=6)\n"
|
|
274
|
-
" cur = conn.cursor()\n"
|
|
275
|
-
" cur.execute(SQL_QUERY)\n"
|
|
276
|
-
" conn.commit()\n"
|
|
277
|
-
" cur.close()\n"
|
|
278
|
-
" conn.close()\n"
|
|
279
|
-
" print('Supabase: Table coap_environmental_records created via PostgreSQL.')\n"
|
|
280
|
-
" return True\n"
|
|
281
230
|
" except Exception as e:\n"
|
|
282
|
-
" print(
|
|
283
|
-
"
|
|
284
|
-
"
|
|
231
|
+
" print('Firebase note:', e)\n"
|
|
232
|
+
" if SUPABASE_URL and 'your-project' not in SUPABASE_URL:\n"
|
|
233
|
+
" print('Supabase configured. Schema is in supabase_schema.sql.')\n"
|
|
234
|
+
" print('Local database: Ready (data/local_coap_env.json)')\n"
|
|
235
|
+
" return True\n\n"
|
|
285
236
|
"if __name__ == '__main__':\n"
|
|
286
|
-
"
|
|
237
|
+
" check()\n"
|
|
287
238
|
)
|
|
288
239
|
|
|
289
240
|
files["dashboard.py"] = (
|
|
@@ -294,11 +245,21 @@ def generate(target_dir: str, mode: str = "learning") -> Dict[str, str]:
|
|
|
294
245
|
"from dotenv import load_dotenv\n\n"
|
|
295
246
|
"load_dotenv()\n\n"
|
|
296
247
|
"st.title('CoAP Environmental Monitoring')\n\n"
|
|
297
|
-
"CLOUD_PROVIDER = os.getenv('CLOUD_PROVIDER', '
|
|
248
|
+
"CLOUD_PROVIDER = os.getenv('CLOUD_PROVIDER', 'firebase').lower().strip()\n"
|
|
249
|
+
"FIREBASE_URL = os.getenv('FIREBASE_DATABASE_URL', '').rstrip('/')\n"
|
|
298
250
|
"SUPABASE_URL = os.getenv('SUPABASE_URL', '').rstrip('/')\n"
|
|
299
251
|
"SUPABASE_KEY = os.getenv('SUPABASE_KEY', '')\n"
|
|
300
252
|
"LOCAL_BACKUP = os.path.join(os.path.dirname(__file__), 'data', 'local_coap_env.json')\n\n"
|
|
301
253
|
"def fetch_records():\n"
|
|
254
|
+
" if CLOUD_PROVIDER == 'firebase' and FIREBASE_URL and 'your-firebase' not in FIREBASE_URL:\n"
|
|
255
|
+
" try:\n"
|
|
256
|
+
" resp = requests.get(f'{FIREBASE_URL}/coap_environmental_records.json', timeout=4)\n"
|
|
257
|
+
" if resp.status_code == 200 and resp.json():\n"
|
|
258
|
+
" data = resp.json()\n"
|
|
259
|
+
" if isinstance(data, dict):\n"
|
|
260
|
+
" return list(reversed(list(data.values())))\n"
|
|
261
|
+
" except Exception:\n"
|
|
262
|
+
" pass\n"
|
|
302
263
|
" if CLOUD_PROVIDER == 'supabase' and SUPABASE_URL and 'your-project' not in SUPABASE_URL:\n"
|
|
303
264
|
" try:\n"
|
|
304
265
|
" headers = {'apikey': SUPABASE_KEY, 'Authorization': f'Bearer {SUPABASE_KEY}'}\n"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|