1#!/usr/bin/env python3
2"""
3IoT Flask REST API ์๋ฒ
4๊ฐ๋จํ ์ผ์ ๋ฐ์ดํฐ ์์ง ๋ฐ ์ฅ์น ์ ์ด API
5
6์ฐธ๊ณ : content/ko/IoT_Embedded/07_HTTP_REST_for_IoT.md
7"""
8
9from flask import Flask, jsonify, request
10from flask_cors import CORS
11from datetime import datetime
12import uuid
13import sqlite3
14import os
15
16app = Flask(__name__)
17CORS(app) # CORS ํ์ฑํ (์น ํด๋ผ์ด์ธํธ ์ง์)
18
19# === ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค์ ===
20DB_PATH = "iot_data.db"
21USE_SQLITE = True # False๋ก ์ค์ ํ๋ฉด ๋ฉ๋ชจ๋ฆฌ ์ ์ฅ์ ์ฌ์ฉ
22
23
24def init_db():
25 """SQLite ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ด๊ธฐํ"""
26 conn = sqlite3.connect(DB_PATH)
27 cursor = conn.cursor()
28
29 # ์ผ์ ํ
์ด๋ธ
30 cursor.execute("""
31 CREATE TABLE IF NOT EXISTS sensors (
32 id TEXT PRIMARY KEY,
33 name TEXT NOT NULL,
34 type TEXT,
35 location TEXT,
36 status TEXT DEFAULT 'active',
37 registered_at TEXT
38 )
39 """)
40
41 # ์ผ์ ๋ฐ์ดํฐ ํ
์ด๋ธ
42 cursor.execute("""
43 CREATE TABLE IF NOT EXISTS sensor_readings (
44 id TEXT PRIMARY KEY,
45 sensor_id TEXT NOT NULL,
46 data TEXT NOT NULL,
47 timestamp TEXT NOT NULL,
48 FOREIGN KEY (sensor_id) REFERENCES sensors(id)
49 )
50 """)
51
52 # ์ฅ์น ํ
์ด๋ธ
53 cursor.execute("""
54 CREATE TABLE IF NOT EXISTS devices (
55 id TEXT PRIMARY KEY,
56 name TEXT NOT NULL,
57 type TEXT,
58 status TEXT DEFAULT 'offline',
59 created_at TEXT,
60 last_seen TEXT
61 )
62 """)
63
64 conn.commit()
65 conn.close()
66
67
68# ๋ฉ๋ชจ๋ฆฌ ์ ์ฅ์ (์๋ฎฌ๋ ์ด์
๋ชจ๋)
69memory_store = {
70 'sensors': {},
71 'sensor_readings': [],
72 'devices': {}
73}
74
75
76# === ํฌํผ ํจ์ ===
77def get_db_connection():
78 """๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ ํ๋"""
79 conn = sqlite3.connect(DB_PATH)
80 conn.row_factory = sqlite3.Row
81 return conn
82
83
84def dict_from_row(row):
85 """sqlite3.Row๋ฅผ ๋์
๋๋ฆฌ๋ก ๋ณํ"""
86 return dict(zip(row.keys(), row))
87
88
89# === API ์๋ํฌ์ธํธ ===
90
91@app.route('/')
92def index():
93 """API ์ ๋ณด"""
94 return jsonify({
95 "name": "IoT REST API Server",
96 "version": "1.0",
97 "storage": "SQLite" if USE_SQLITE else "Memory",
98 "endpoints": {
99 "/": "GET - API ์ ๋ณด",
100 "/health": "GET - ํฌ์ค ์ฒดํฌ",
101 "/api/sensors": "GET, POST - ์ผ์ ๋ชฉ๋ก/๋ฑ๋ก",
102 "/api/sensors/<id>": "GET - ์ผ์ ์ ๋ณด ์กฐํ",
103 "/api/sensors/<id>/data": "GET, POST - ์ผ์ ๋ฐ์ดํฐ ์กฐํ/๋ฑ๋ก",
104 "/api/sensors/<id>/latest": "GET - ์ต์ ์ผ์ ๋ฐ์ดํฐ",
105 "/api/sensors/<id>/stats": "GET - ์ผ์ ํต๊ณ",
106 "/api/devices": "GET, POST - ์ฅ์น ๋ชฉ๋ก/๋ฑ๋ก",
107 "/api/devices/<id>": "GET, PUT, DELETE - ์ฅ์น ์กฐํ/์์ /์ญ์ ",
108 "/api/devices/<id>/command": "POST - ์ฅ์น ๋ช
๋ น ์ ์ก"
109 }
110 })
111
112
113@app.route('/health')
114def health():
115 """ํฌ์ค ์ฒดํฌ"""
116 if USE_SQLITE:
117 # DB ์ฐ๊ฒฐ ํ์ธ
118 try:
119 conn = get_db_connection()
120 conn.close()
121 status = "healthy"
122 except Exception as e:
123 status = f"unhealthy: {str(e)}"
124 else:
125 status = "healthy (memory mode)"
126
127 return jsonify({
128 "status": status,
129 "timestamp": datetime.now().isoformat(),
130 "storage": "SQLite" if USE_SQLITE else "Memory"
131 })
132
133
134# === ์ผ์ API ===
135
136@app.route('/api/sensors', methods=['GET'])
137def list_sensors():
138 """๋ฑ๋ก๋ ์ผ์ ๋ชฉ๋ก ์กฐํ"""
139 if USE_SQLITE:
140 conn = get_db_connection()
141 cursor = conn.cursor()
142 cursor.execute("SELECT * FROM sensors")
143 sensors = [dict_from_row(row) for row in cursor.fetchall()]
144 conn.close()
145 else:
146 sensors = list(memory_store['sensors'].values())
147
148 return jsonify({
149 "sensors": sensors,
150 "count": len(sensors)
151 })
152
153
154@app.route('/api/sensors', methods=['POST'])
155def register_sensor():
156 """์ ์ผ์ ๋ฑ๋ก"""
157 data = request.get_json()
158
159 if not data or 'name' not in data:
160 return jsonify({"error": "name is required"}), 400
161
162 sensor_id = str(uuid.uuid4())[:8]
163 sensor = {
164 "id": sensor_id,
165 "name": data['name'],
166 "type": data.get('type', 'generic'),
167 "location": data.get('location', 'unknown'),
168 "status": "active",
169 "registered_at": datetime.now().isoformat()
170 }
171
172 if USE_SQLITE:
173 conn = get_db_connection()
174 cursor = conn.cursor()
175 cursor.execute("""
176 INSERT INTO sensors (id, name, type, location, status, registered_at)
177 VALUES (?, ?, ?, ?, ?, ?)
178 """, (sensor['id'], sensor['name'], sensor['type'],
179 sensor['location'], sensor['status'], sensor['registered_at']))
180 conn.commit()
181 conn.close()
182 else:
183 memory_store['sensors'][sensor_id] = sensor
184
185 return jsonify(sensor), 201
186
187
188@app.route('/api/sensors/<sensor_id>', methods=['GET'])
189def get_sensor(sensor_id):
190 """์ผ์ ์ ๋ณด ์กฐํ"""
191 if USE_SQLITE:
192 conn = get_db_connection()
193 cursor = conn.cursor()
194 cursor.execute("SELECT * FROM sensors WHERE id = ?", (sensor_id,))
195 row = cursor.fetchone()
196 conn.close()
197
198 if not row:
199 return jsonify({"error": "Sensor not found"}), 404
200
201 sensor = dict_from_row(row)
202 else:
203 sensor = memory_store['sensors'].get(sensor_id)
204 if not sensor:
205 return jsonify({"error": "Sensor not found"}), 404
206
207 return jsonify(sensor)
208
209
210@app.route('/api/sensors/<sensor_id>/data', methods=['POST'])
211def post_sensor_data(sensor_id):
212 """์ผ์ ๋ฐ์ดํฐ ์์ """
213 import json
214
215 data = request.get_json()
216
217 if not data:
218 return jsonify({"error": "No data provided"}), 400
219
220 reading_id = str(uuid.uuid4())
221 reading = {
222 "id": reading_id,
223 "sensor_id": sensor_id,
224 "data": data,
225 "timestamp": datetime.now().isoformat()
226 }
227
228 if USE_SQLITE:
229 # ์ผ์ ์๋ ๋ฑ๋ก (์กด์ฌํ์ง ์๋ ๊ฒฝ์ฐ)
230 conn = get_db_connection()
231 cursor = conn.cursor()
232 cursor.execute("SELECT id FROM sensors WHERE id = ?", (sensor_id,))
233 if not cursor.fetchone():
234 cursor.execute("""
235 INSERT INTO sensors (id, name, type, status, registered_at)
236 VALUES (?, ?, ?, ?, ?)
237 """, (sensor_id, f"auto_{sensor_id}", "generic",
238 "active", datetime.now().isoformat()))
239
240 # ์ผ์ ๋ฐ์ดํฐ ์ ์ฅ
241 cursor.execute("""
242 INSERT INTO sensor_readings (id, sensor_id, data, timestamp)
243 VALUES (?, ?, ?, ?)
244 """, (reading['id'], reading['sensor_id'],
245 json.dumps(reading['data']), reading['timestamp']))
246 conn.commit()
247 conn.close()
248 else:
249 # ์ผ์ ์๋ ๋ฑ๋ก
250 if sensor_id not in memory_store['sensors']:
251 memory_store['sensors'][sensor_id] = {
252 "id": sensor_id,
253 "name": f"auto_{sensor_id}",
254 "type": "generic",
255 "status": "active",
256 "registered_at": datetime.now().isoformat()
257 }
258
259 memory_store['sensor_readings'].append(reading)
260
261 # ์ต๊ทผ 1000๊ฐ๋ง ์ ์ง
262 if len(memory_store['sensor_readings']) > 1000:
263 memory_store['sensor_readings'].pop(0)
264
265 return jsonify({"status": "ok", "reading_id": reading['id']}), 201
266
267
268@app.route('/api/sensors/<sensor_id>/data', methods=['GET'])
269def get_sensor_data(sensor_id):
270 """์ผ์ ๋ฐ์ดํฐ ์กฐํ"""
271 import json
272
273 # ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ
274 limit = request.args.get('limit', 100, type=int)
275 since = request.args.get('since', None) # ISO timestamp
276
277 if USE_SQLITE:
278 conn = get_db_connection()
279 cursor = conn.cursor()
280
281 query = "SELECT * FROM sensor_readings WHERE sensor_id = ?"
282 params = [sensor_id]
283
284 if since:
285 query += " AND timestamp > ?"
286 params.append(since)
287
288 query += " ORDER BY timestamp DESC LIMIT ?"
289 params.append(limit)
290
291 cursor.execute(query, params)
292 rows = cursor.fetchall()
293 conn.close()
294
295 readings = []
296 for row in rows:
297 reading_dict = dict_from_row(row)
298 reading_dict['data'] = json.loads(reading_dict['data'])
299 readings.append(reading_dict)
300 else:
301 # ํํฐ๋ง
302 readings = [r for r in memory_store['sensor_readings']
303 if r['sensor_id'] == sensor_id]
304
305 if since:
306 readings = [r for r in readings if r['timestamp'] > since]
307
308 # ์ต์ ์ ์ ๋ ฌ ๋ฐ ์ ํ
309 readings = sorted(readings, key=lambda x: x['timestamp'],
310 reverse=True)[:limit]
311
312 return jsonify({
313 "sensor_id": sensor_id,
314 "readings": readings,
315 "count": len(readings)
316 })
317
318
319@app.route('/api/sensors/<sensor_id>/latest', methods=['GET'])
320def get_latest_reading(sensor_id):
321 """์ต์ ์ผ์ ๋ฐ์ดํฐ ์กฐํ"""
322 import json
323
324 if USE_SQLITE:
325 conn = get_db_connection()
326 cursor = conn.cursor()
327 cursor.execute("""
328 SELECT * FROM sensor_readings
329 WHERE sensor_id = ?
330 ORDER BY timestamp DESC
331 LIMIT 1
332 """, (sensor_id,))
333 row = cursor.fetchone()
334 conn.close()
335
336 if not row:
337 return jsonify({"error": "No data found"}), 404
338
339 latest = dict_from_row(row)
340 latest['data'] = json.loads(latest['data'])
341 else:
342 readings = [r for r in memory_store['sensor_readings']
343 if r['sensor_id'] == sensor_id]
344
345 if not readings:
346 return jsonify({"error": "No data found"}), 404
347
348 latest = max(readings, key=lambda x: x['timestamp'])
349
350 return jsonify(latest)
351
352
353@app.route('/api/sensors/<sensor_id>/stats', methods=['GET'])
354def get_sensor_stats(sensor_id):
355 """์ผ์ ๋ฐ์ดํฐ ํต๊ณ (์ซ์ ํ๋)"""
356 import json
357
358 field = request.args.get('field', 'temperature')
359
360 if USE_SQLITE:
361 conn = get_db_connection()
362 cursor = conn.cursor()
363 cursor.execute("""
364 SELECT data FROM sensor_readings WHERE sensor_id = ?
365 """, (sensor_id,))
366 rows = cursor.fetchall()
367 conn.close()
368
369 values = []
370 for row in rows:
371 data = json.loads(row['data'])
372 if field in data:
373 try:
374 values.append(float(data[field]))
375 except (ValueError, TypeError):
376 pass
377 else:
378 readings = [r for r in memory_store['sensor_readings']
379 if r['sensor_id'] == sensor_id]
380
381 values = []
382 for r in readings:
383 if field in r.get('data', {}):
384 try:
385 values.append(float(r['data'][field]))
386 except (ValueError, TypeError):
387 pass
388
389 if not values:
390 return jsonify({"error": f"No numeric data for field: {field}"}), 404
391
392 stats = {
393 "sensor_id": sensor_id,
394 "field": field,
395 "count": len(values),
396 "min": min(values),
397 "max": max(values),
398 "avg": sum(values) / len(values),
399 "latest": values[-1] if values else None
400 }
401
402 return jsonify(stats)
403
404
405# === ์ฅ์น API ===
406
407@app.route('/api/devices', methods=['GET'])
408def list_devices():
409 """์ฅ์น ๋ชฉ๋ก ์กฐํ"""
410 # ํํฐ๋ง
411 device_type = request.args.get('type')
412 status = request.args.get('status')
413
414 if USE_SQLITE:
415 conn = get_db_connection()
416 cursor = conn.cursor()
417
418 query = "SELECT * FROM devices WHERE 1=1"
419 params = []
420
421 if device_type:
422 query += " AND type = ?"
423 params.append(device_type)
424 if status:
425 query += " AND status = ?"
426 params.append(status)
427
428 cursor.execute(query, params)
429 devices = [dict_from_row(row) for row in cursor.fetchall()]
430 conn.close()
431 else:
432 devices = list(memory_store['devices'].values())
433
434 if device_type:
435 devices = [d for d in devices if d.get('type') == device_type]
436 if status:
437 devices = [d for d in devices if d.get('status') == status]
438
439 return jsonify({
440 "devices": devices,
441 "total": len(devices)
442 })
443
444
445@app.route('/api/devices', methods=['POST'])
446def create_device():
447 """์ฅ์น ๋ฑ๋ก"""
448 data = request.get_json()
449
450 required_fields = ['id', 'name', 'type']
451 for field in required_fields:
452 if field not in data:
453 return jsonify({"error": f"Missing field: {field}"}), 400
454
455 device_id = data['id']
456
457 if USE_SQLITE:
458 conn = get_db_connection()
459 cursor = conn.cursor()
460 cursor.execute("SELECT id FROM devices WHERE id = ?", (device_id,))
461 if cursor.fetchone():
462 conn.close()
463 return jsonify({"error": "Device already exists"}), 409
464 else:
465 if device_id in memory_store['devices']:
466 return jsonify({"error": "Device already exists"}), 409
467
468 device = {
469 "id": device_id,
470 "name": data['name'],
471 "type": data['type'],
472 "status": "offline",
473 "created_at": datetime.now().isoformat(),
474 "last_seen": None
475 }
476
477 if USE_SQLITE:
478 cursor.execute("""
479 INSERT INTO devices (id, name, type, status, created_at, last_seen)
480 VALUES (?, ?, ?, ?, ?, ?)
481 """, (device['id'], device['name'], device['type'],
482 device['status'], device['created_at'], device['last_seen']))
483 conn.commit()
484 conn.close()
485 else:
486 memory_store['devices'][device_id] = device
487
488 return jsonify(device), 201
489
490
491@app.route('/api/devices/<device_id>', methods=['GET'])
492def get_device(device_id):
493 """์ฅ์น ์ ๋ณด ์กฐํ"""
494 if USE_SQLITE:
495 conn = get_db_connection()
496 cursor = conn.cursor()
497 cursor.execute("SELECT * FROM devices WHERE id = ?", (device_id,))
498 row = cursor.fetchone()
499 conn.close()
500
501 if not row:
502 return jsonify({"error": "Device not found"}), 404
503
504 device = dict_from_row(row)
505 else:
506 device = memory_store['devices'].get(device_id)
507 if not device:
508 return jsonify({"error": "Device not found"}), 404
509
510 return jsonify(device)
511
512
513@app.route('/api/devices/<device_id>', methods=['PUT'])
514def update_device(device_id):
515 """์ฅ์น ์ ๋ณด ์ ์ฒด ์์ """
516 if USE_SQLITE:
517 conn = get_db_connection()
518 cursor = conn.cursor()
519 cursor.execute("SELECT id FROM devices WHERE id = ?", (device_id,))
520 if not cursor.fetchone():
521 conn.close()
522 return jsonify({"error": "Device not found"}), 404
523 else:
524 if device_id not in memory_store['devices']:
525 return jsonify({"error": "Device not found"}), 404
526
527 data = request.get_json()
528 data['id'] = device_id # ID ์ ์ง
529 data['updated_at'] = datetime.now().isoformat()
530
531 if USE_SQLITE:
532 cursor.execute("""
533 UPDATE devices
534 SET name = ?, type = ?, status = ?
535 WHERE id = ?
536 """, (data.get('name'), data.get('type'),
537 data.get('status'), device_id))
538 conn.commit()
539 conn.close()
540
541 # ์
๋ฐ์ดํธ๋ ์ฅ์น ์กฐํ
542 return get_device(device_id)
543 else:
544 memory_store['devices'][device_id] = data
545 return jsonify(data)
546
547
548@app.route('/api/devices/<device_id>', methods=['DELETE'])
549def delete_device(device_id):
550 """์ฅ์น ์ญ์ """
551 if USE_SQLITE:
552 conn = get_db_connection()
553 cursor = conn.cursor()
554 cursor.execute("SELECT id FROM devices WHERE id = ?", (device_id,))
555 if not cursor.fetchone():
556 conn.close()
557 return jsonify({"error": "Device not found"}), 404
558
559 cursor.execute("DELETE FROM devices WHERE id = ?", (device_id,))
560 conn.commit()
561 conn.close()
562 else:
563 if device_id not in memory_store['devices']:
564 return jsonify({"error": "Device not found"}), 404
565
566 del memory_store['devices'][device_id]
567
568 return '', 204
569
570
571@app.route('/api/devices/<device_id>/command', methods=['POST'])
572def send_device_command(device_id):
573 """์ฅ์น์ ๋ช
๋ น ์ ์ก (์๋ฎฌ๋ ์ด์
)"""
574 if USE_SQLITE:
575 conn = get_db_connection()
576 cursor = conn.cursor()
577 cursor.execute("SELECT id FROM devices WHERE id = ?", (device_id,))
578 if not cursor.fetchone():
579 conn.close()
580 return jsonify({"error": "Device not found"}), 404
581 conn.close()
582 else:
583 if device_id not in memory_store['devices']:
584 return jsonify({"error": "Device not found"}), 404
585
586 data = request.get_json()
587
588 if 'command' not in data:
589 return jsonify({"error": "Command required"}), 400
590
591 # ๋ช
๋ น ์์ฑ (์ค์ ๋ก๋ MQTT ๋ฐํ ๋ฑ)
592 command = {
593 "device_id": device_id,
594 "command": data['command'],
595 "params": data.get('params', {}),
596 "sent_at": datetime.now().isoformat()
597 }
598
599 # ์๋ฎฌ๋ ์ด์
: ๋ช
๋ น ์ถ๋ ฅ
600 print(f"[๋ช
๋ น ์ ์ก] {device_id}: {command['command']}")
601
602 return jsonify({
603 "status": "sent",
604 "command": command
605 }), 202
606
607
608# === ๋ฉ์ธ ์คํ ===
609
610if __name__ == "__main__":
611 # SQLite ๋ชจ๋์ธ ๊ฒฝ์ฐ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ด๊ธฐํ
612 if USE_SQLITE:
613 init_db()
614 print(f"๋ฐ์ดํฐ๋ฒ ์ด์ค ์ด๊ธฐํ: {DB_PATH}")
615 else:
616 print("๋ฉ๋ชจ๋ฆฌ ์ ์ฅ์ ๋ชจ๋ (์๋ฎฌ๋ ์ด์
)")
617
618 print("\n=== IoT REST API ์๋ฒ ์์ ===")
619 print("์๋ํฌ์ธํธ: http://localhost:5000/")
620 print("API ๋ฌธ์: http://localhost:5000/")
621 print("\n์ข
๋ฃ: Ctrl+C")
622
623 # Flask ์๋ฒ ์์
624 app.run(host='0.0.0.0', port=5000, debug=True)