http_server.py

Download
python 625 lines 17.6 KB
  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)