1#!/usr/bin/env python3
2"""
3์ค๋งํธํ ์๋ํ ์์คํ
(Smart Home Automation System)
4
5ํตํฉ ํ ์๋ํ ์์คํ
์ผ๋ก ์กฐ๋ช
์ ์ด, ํ๊ฒฝ ๋ชจ๋ํฐ๋ง, MQTT ๊ธฐ๋ฐ ์ฅ์น ํต์ ์ ์ ๊ณตํฉ๋๋ค.
6์๋ฎฌ๋ ์ด์
๋ชจ๋๋ฅผ ์ง์ํ์ฌ ์ค์ ํ๋์จ์ด ์์ด๋ ๋์ ๊ฐ๋ฅํฉ๋๋ค.
7
8์ฃผ์ ๊ธฐ๋ฅ:
9- ๋ฆด๋ ์ด๋ฅผ ํตํ ์กฐ๋ช
/๊ฐ์ ์ ์ด (์๋ฎฌ๋ ์ด์
๋ชจ๋ ์ง์)
10- ์จ์ต๋ ์ผ์ ๋ชจ๋ํฐ๋ง (์๋ฎฌ๋ ์ด์
๋ฐ์ดํฐ ์์ฑ)
11- MQTT ๊ธฐ๋ฐ ์ฅ์น ํต์ ๋ฐ ์ ์ด
12- ์๋ํ ๊ท์น ์์ง (์จ๋ ๊ธฐ๋ฐ, ๋ชจ์
๊ธฐ๋ฐ)
13- ์น ๋์๋ณด๋ JSON API ์ ๊ณต
14- ์ํ ๊ด๋ฆฌ ๋ฐ ๋ก๊น
15
16์ฌ์ฉ๋ฒ:
17 # ์๋ฎฌ๋ ์ด์
๋ชจ๋ (ํ๋์จ์ด ๋ถํ์)
18 python home_automation.py --simulate
19
20 # ์ค์ ํ๋์จ์ด ๋ชจ๋
21 python home_automation.py
22
23 # MQTT ๋ธ๋ก์ปค ์ง์
24 python home_automation.py --broker mqtt.example.com --simulate
25"""
26
27import time
28import json
29import random
30import threading
31import logging
32from datetime import datetime
33from dataclasses import dataclass, asdict
34from typing import Dict, List, Optional, Callable
35from queue import Queue
36import argparse
37
38# ์๋ฎฌ๋ ์ด์
๋ชจ๋ ํ๋๊ทธ
39SIMULATION_MODE = True
40
41# ์ค์ ํ๋์จ์ด ๋ผ์ด๋ธ๋ฌ๋ฆฌ (์๋ฎฌ๋ ์ด์
๋ชจ๋์์๋ ์ฌ์ฉ ์ ํจ)
42try:
43 from gpiozero import OutputDevice
44 import adafruit_dht
45 import board
46 HARDWARE_AVAILABLE = True
47except ImportError:
48 HARDWARE_AVAILABLE = False
49 print("ํ๋์จ์ด ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์์. ์๋ฎฌ๋ ์ด์
๋ชจ๋๋ก ์คํ๋ฉ๋๋ค.")
50
51try:
52 import paho.mqtt.client as mqtt
53 MQTT_AVAILABLE = True
54except ImportError:
55 MQTT_AVAILABLE = False
56 print("paho-mqtt ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ์์ต๋๋ค. MQTT ๊ธฐ๋ฅ์ด ๋นํ์ฑํ๋ฉ๋๋ค.")
57
58
59# ============================================================
60# ๋ฐ์ดํฐ ๋ชจ๋ธ
61# ============================================================
62
63@dataclass
64class Light:
65 """์กฐ๋ช
์ฅ์น ๋ฐ์ดํฐ ํด๋์ค"""
66 id: str
67 name: str
68 gpio_pin: int
69 location: str
70 is_on: bool = False
71
72 def to_dict(self) -> dict:
73 """๋์
๋๋ฆฌ ๋ณํ"""
74 return asdict(self)
75
76
77@dataclass
78class SensorReading:
79 """์ผ์ ๋ฐ์ดํฐ ํด๋์ค"""
80 sensor_id: str
81 temperature: float
82 humidity: float
83 timestamp: datetime
84
85 def to_dict(self) -> dict:
86 """๋์
๋๋ฆฌ ๋ณํ"""
87 return {
88 "sensor_id": self.sensor_id,
89 "temperature": self.temperature,
90 "humidity": self.humidity,
91 "timestamp": self.timestamp.isoformat()
92 }
93
94
95# ============================================================
96# ์กฐ๋ช
์ ์ด๊ธฐ (Light Controller)
97# ============================================================
98
99class LightController:
100 """
101 ์กฐ๋ช
์ ์ด ํด๋์ค
102
103 ๋ฆด๋ ์ด๋ฅผ ํตํด ์กฐ๋ช
์ ์ ์ดํฉ๋๋ค.
104 ์๋ฎฌ๋ ์ด์
๋ชจ๋์์๋ ์ค์ GPIO ๋์ ์ํ๋ง ๊ด๋ฆฌํฉ๋๋ค.
105 """
106
107 def __init__(self, config: dict, simulate: bool = True):
108 """
109 ์ด๊ธฐํ
110
111 Args:
112 config: ์กฐ๋ช
์ค์ (lights ๋ฆฌ์คํธ ํฌํจ)
113 simulate: ์๋ฎฌ๋ ์ด์
๋ชจ๋ ์ฌ๋ถ
114 """
115 self.simulate = simulate
116 self.lights: Dict[str, Light] = {}
117 self.relays: Dict[str, any] = {}
118
119 # ์กฐ๋ช
์ค์
120 for light_config in config.get('lights', []):
121 light = Light(**light_config)
122 self.lights[light.id] = light
123
124 # ๋ฆด๋ ์ด ์ด๊ธฐํ
125 if not simulate and HARDWARE_AVAILABLE:
126 # ์ค์ ํ๋์จ์ด: gpiozero ์ฌ์ฉ
127 relay = OutputDevice(
128 light.gpio_pin,
129 active_high=False, # Active Low ๋ฆด๋ ์ด
130 initial_value=False
131 )
132 self.relays[light.id] = relay
133 else:
134 # ์๋ฎฌ๋ ์ด์
: None ์ ์ฅ
135 self.relays[light.id] = None
136
137 logging.info(f"LightController ์ด๊ธฐํ ์๋ฃ (์๋ฎฌ๋ ์ด์
={simulate}, ์กฐ๋ช
={len(self.lights)}๊ฐ)")
138
139 def turn_on(self, light_id: str) -> bool:
140 """
141 ์กฐ๋ช
์ผ๊ธฐ
142
143 Args:
144 light_id: ์กฐ๋ช
ID
145
146 Returns:
147 ์ฑ๊ณต ์ฌ๋ถ
148 """
149 if light_id not in self.lights:
150 logging.warning(f"์กฐ๋ช
ID '{light_id}' ์์")
151 return False
152
153 if self.simulate:
154 # ์๋ฎฌ๋ ์ด์
: ์ํ๋ง ๋ณ๊ฒฝ
155 self.lights[light_id].is_on = True
156 logging.info(f"[์๋ฎฌ] ์กฐ๋ช
ON: {self.lights[light_id].name}")
157 else:
158 # ์ค์ ํ๋์จ์ด: ๋ฆด๋ ์ด ์ ์ด
159 self.relays[light_id].on()
160 self.lights[light_id].is_on = True
161 logging.info(f"์กฐ๋ช
ON: {self.lights[light_id].name}")
162
163 return True
164
165 def turn_off(self, light_id: str) -> bool:
166 """
167 ์กฐ๋ช
๋๊ธฐ
168
169 Args:
170 light_id: ์กฐ๋ช
ID
171
172 Returns:
173 ์ฑ๊ณต ์ฌ๋ถ
174 """
175 if light_id not in self.lights:
176 logging.warning(f"์กฐ๋ช
ID '{light_id}' ์์")
177 return False
178
179 if self.simulate:
180 # ์๋ฎฌ๋ ์ด์
181 self.lights[light_id].is_on = False
182 logging.info(f"[์๋ฎฌ] ์กฐ๋ช
OFF: {self.lights[light_id].name}")
183 else:
184 # ์ค์ ํ๋์จ์ด
185 self.relays[light_id].off()
186 self.lights[light_id].is_on = False
187 logging.info(f"์กฐ๋ช
OFF: {self.lights[light_id].name}")
188
189 return True
190
191 def toggle(self, light_id: str) -> bool:
192 """
193 ์กฐ๋ช
ํ ๊ธ
194
195 Args:
196 light_id: ์กฐ๋ช
ID
197
198 Returns:
199 ์ฑ๊ณต ์ฌ๋ถ
200 """
201 if light_id not in self.lights:
202 return False
203
204 if self.lights[light_id].is_on:
205 return self.turn_off(light_id)
206 else:
207 return self.turn_on(light_id)
208
209 def get_status(self, light_id: str = None) -> Optional[dict]:
210 """
211 ์กฐ๋ช
์ํ ์กฐํ
212
213 Args:
214 light_id: ์กฐ๋ช
ID (None์ด๋ฉด ์ ์ฒด)
215
216 Returns:
217 ์ํ ๋์
๋๋ฆฌ
218 """
219 if light_id:
220 light = self.lights.get(light_id)
221 if light:
222 return light.to_dict()
223 return None
224
225 # ์ ์ฒด ์กฐ๋ช
์ํ
226 return {
227 "lights": [light.to_dict() for light in self.lights.values()]
228 }
229
230 def all_off(self):
231 """๋ชจ๋ ์กฐ๋ช
๋๊ธฐ"""
232 for light_id in self.lights:
233 self.turn_off(light_id)
234 logging.info("๋ชจ๋ ์กฐ๋ช
OFF")
235
236 def all_on(self):
237 """๋ชจ๋ ์กฐ๋ช
์ผ๊ธฐ"""
238 for light_id in self.lights:
239 self.turn_on(light_id)
240 logging.info("๋ชจ๋ ์กฐ๋ช
ON")
241
242 def cleanup(self):
243 """์ ๋ฆฌ (ํ๋ก๊ทธ๋จ ์ข
๋ฃ ์ ํธ์ถ)"""
244 if not self.simulate:
245 for relay in self.relays.values():
246 if relay:
247 relay.close()
248 logging.info("LightController ์ ๋ฆฌ ์๋ฃ")
249
250
251# ============================================================
252# ํ๊ฒฝ ์ผ์ ๋ชจ๋ํฐ (Environment Monitor)
253# ============================================================
254
255class EnvironmentMonitor:
256 """
257 ํ๊ฒฝ ์ผ์ ๋ชจ๋ํฐ๋ง ํด๋์ค
258
259 DHT11 ์ผ์๋ก ์จ๋/์ต๋๋ฅผ ์ฃผ๊ธฐ์ ์ผ๋ก ์ฝ๊ณ ํ์คํ ๋ฆฌ๋ฅผ ์ ์ฅํฉ๋๋ค.
260 ์๋ฎฌ๋ ์ด์
๋ชจ๋์์๋ ๋๋ค ๋ฐ์ดํฐ๋ฅผ ์์ฑํฉ๋๋ค.
261 """
262
263 def __init__(self, sensor_pin: int = 4, sensor_id: str = "env_01", simulate: bool = True):
264 """
265 ์ด๊ธฐํ
266
267 Args:
268 sensor_pin: GPIO ํ ๋ฒํธ
269 sensor_id: ์ผ์ ID
270 simulate: ์๋ฎฌ๋ ์ด์
๋ชจ๋ ์ฌ๋ถ
271 """
272 self.sensor_id = sensor_id
273 self.sensor_pin = sensor_pin
274 self.simulate = simulate
275
276 # DHT ์ผ์ ์ด๊ธฐํ
277 self.dht = None
278 if not simulate and HARDWARE_AVAILABLE:
279 try:
280 self.dht = adafruit_dht.DHT11(getattr(board, f"D{sensor_pin}"))
281 except Exception as e:
282 logging.error(f"DHT ์ผ์ ์ด๊ธฐํ ์คํจ: {e}")
283 self.simulate = True
284
285 # ๋ฐ์ดํฐ ํ ๋ฐ ํ์คํ ๋ฆฌ
286 self.data_queue = Queue()
287 self.latest_reading: Optional[SensorReading] = None
288 self.readings_history: List[SensorReading] = []
289 self.max_history = 1000
290
291 # ์ค๋ ๋ ์ ์ด
292 self.running = False
293 self.thread = None
294
295 # ์๋ฎฌ๋ ์ด์
์ฉ ํ์ฌ ๊ฐ
296 self.sim_temperature = 25.0
297 self.sim_humidity = 60.0
298
299 logging.info(f"EnvironmentMonitor ์ด๊ธฐํ (์๋ฎฌ๋ ์ด์
={simulate})")
300
301 def read_sensor(self) -> Optional[SensorReading]:
302 """
303 ์ผ์ ์ฝ๊ธฐ
304
305 Returns:
306 ์ผ์ ๋ฐ์ดํฐ ๋๋ None (์คํจ ์)
307 """
308 if self.simulate:
309 # ์๋ฎฌ๋ ์ด์
: ๋๋ค ๋ณํ ์์ฑ
310 self.sim_temperature += random.uniform(-0.5, 0.5)
311 self.sim_temperature = max(10, min(40, self.sim_temperature))
312
313 self.sim_humidity += random.uniform(-2, 2)
314 self.sim_humidity = max(30, min(90, self.sim_humidity))
315
316 reading = SensorReading(
317 sensor_id=self.sensor_id,
318 temperature=round(self.sim_temperature, 1),
319 humidity=round(self.sim_humidity, 1),
320 timestamp=datetime.now()
321 )
322 return reading
323
324 else:
325 # ์ค์ ์ผ์
326 try:
327 temperature = self.dht.temperature
328 humidity = self.dht.humidity
329
330 if temperature is not None and humidity is not None:
331 reading = SensorReading(
332 sensor_id=self.sensor_id,
333 temperature=temperature,
334 humidity=humidity,
335 timestamp=datetime.now()
336 )
337 return reading
338
339 except RuntimeError as e:
340 # DHT ์ผ์๋ ๊ฐ๋ ์ฝ๊ธฐ ์คํจ (์ ์)
341 logging.debug(f"์ผ์ ์ฝ๊ธฐ ์คํจ (์ ์): {e}")
342 except Exception as e:
343 logging.error(f"์ผ์ ์ฝ๊ธฐ ์ค๋ฅ: {e}")
344
345 return None
346
347 def _monitor_loop(self, interval: int):
348 """
349 ๋ชจ๋ํฐ๋ง ๋ฃจํ (๋ฐฑ๊ทธ๋ผ์ด๋ ์ค๋ ๋)
350
351 Args:
352 interval: ์ฝ๊ธฐ ๊ฐ๊ฒฉ (์ด)
353 """
354 while self.running:
355 reading = self.read_sensor()
356
357 if reading:
358 # ์ต์ ๋ฐ์ดํฐ ์
๋ฐ์ดํธ
359 self.latest_reading = reading
360
361 # ํ์คํ ๋ฆฌ ์ ์ฅ
362 self.readings_history.append(reading)
363 if len(self.readings_history) > self.max_history:
364 self.readings_history.pop(0)
365
366 # ํ์ ์ถ๊ฐ (์ธ๋ถ ๊ตฌ๋
์์ฉ)
367 self.data_queue.put(reading)
368
369 time.sleep(interval)
370
371 def start(self, interval: int = 5):
372 """
373 ๋ชจ๋ํฐ๋ง ์์
374
375 Args:
376 interval: ์ฝ๊ธฐ ๊ฐ๊ฒฉ (์ด)
377 """
378 if self.running:
379 logging.warning("์ด๋ฏธ ๋ชจ๋ํฐ๋ง ์ค์
๋๋ค")
380 return
381
382 self.running = True
383 self.thread = threading.Thread(
384 target=self._monitor_loop,
385 args=(interval,),
386 daemon=True
387 )
388 self.thread.start()
389 logging.info(f"ํ๊ฒฝ ๋ชจ๋ํฐ๋ง ์์ (๊ฐ๊ฒฉ={interval}์ด)")
390
391 def stop(self):
392 """๋ชจ๋ํฐ๋ง ์ค์ง"""
393 if not self.running:
394 return
395
396 self.running = False
397 if self.thread:
398 self.thread.join(timeout=5)
399
400 if self.dht and not self.simulate:
401 self.dht.exit()
402
403 logging.info("ํ๊ฒฝ ๋ชจ๋ํฐ๋ง ์ค์ง")
404
405 def get_latest(self) -> Optional[dict]:
406 """์ต์ ์ผ์ ๋ฐ์ดํฐ ๋ฐํ"""
407 if self.latest_reading:
408 return self.latest_reading.to_dict()
409 return None
410
411 def get_stats(self) -> dict:
412 """
413 ํต๊ณ ๋ฐ์ดํฐ ๋ฐํ
414
415 Returns:
416 ์ต์/์ต๋/ํ๊ท ํต๊ณ
417 """
418 if not self.readings_history:
419 return {}
420
421 temps = [r.temperature for r in self.readings_history]
422 humids = [r.humidity for r in self.readings_history]
423
424 return {
425 "count": len(self.readings_history),
426 "temperature": {
427 "min": round(min(temps), 1),
428 "max": round(max(temps), 1),
429 "avg": round(sum(temps) / len(temps), 1)
430 },
431 "humidity": {
432 "min": round(min(humids), 1),
433 "max": round(max(humids), 1),
434 "avg": round(sum(humids) / len(humids), 1)
435 }
436 }
437
438
439# ============================================================
440# MQTT ํธ๋ค๋ฌ
441# ============================================================
442
443class SmartHomeMQTT:
444 """
445 MQTT ๊ธฐ๋ฐ ์ค๋งํธํ ์ ์ด ํธ๋ค๋ฌ
446
447 MQTT ๋ธ๋ก์ปค๋ฅผ ํตํด ์ฅ์น๋ฅผ ์ ์ดํ๊ณ ์ผ์ ๋ฐ์ดํฐ๋ฅผ ๋ฐํํฉ๋๋ค.
448 """
449
450 TOPICS = {
451 "light_command": "home/+/light/command",
452 "light_status": "home/{}/light/status",
453 "sensor_data": "home/sensor/{}",
454 "motion": "home/motion/{}",
455 "system": "home/system/status",
456 "automation": "home/automation/event"
457 }
458
459 def __init__(self, light_controller: LightController,
460 env_monitor: EnvironmentMonitor,
461 broker: str = "localhost",
462 port: int = 1883):
463 """
464 ์ด๊ธฐํ
465
466 Args:
467 light_controller: ์กฐ๋ช
์ ์ด๊ธฐ
468 env_monitor: ํ๊ฒฝ ๋ชจ๋ํฐ
469 broker: MQTT ๋ธ๋ก์ปค ์ฃผ์
470 port: MQTT ํฌํธ
471 """
472 self.light_controller = light_controller
473 self.env_monitor = env_monitor
474 self.broker = broker
475 self.port = port
476
477 if not MQTT_AVAILABLE:
478 logging.warning("MQTT ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์์. MQTT ๊ธฐ๋ฅ ๋นํ์ฑํ")
479 self.client = None
480 return
481
482 # MQTT ํด๋ผ์ด์ธํธ ์์ฑ
483 self.client = mqtt.Client(client_id="smart_home_gateway")
484 self.client.on_connect = self._on_connect
485 self.client.on_message = self._on_message
486
487 # Last Will and Testament (LWT) ์ค์
488 self.client.will_set(
489 self.TOPICS["system"],
490 json.dumps({"status": "offline"}),
491 qos=1,
492 retain=True
493 )
494
495 logging.info(f"MQTT ํด๋ผ์ด์ธํธ ์์ฑ (๋ธ๋ก์ปค={broker}:{port})")
496
497 def connect(self):
498 """MQTT ๋ธ๋ก์ปค ์ฐ๊ฒฐ"""
499 if not self.client:
500 return
501
502 try:
503 self.client.connect(self.broker, self.port)
504 logging.info("MQTT ๋ธ๋ก์ปค ์ฐ๊ฒฐ ์๋ ์ค...")
505 except Exception as e:
506 logging.error(f"MQTT ์ฐ๊ฒฐ ์คํจ: {e}")
507
508 def _on_connect(self, client, userdata, flags, rc):
509 """MQTT ์ฐ๊ฒฐ ์ฝ๋ฐฑ"""
510 if rc == 0:
511 logging.info("MQTT ๋ธ๋ก์ปค ์ฐ๊ฒฐ ์ฑ๊ณต")
512
513 # ํ ํฝ ๊ตฌ๋
514 client.subscribe(self.TOPICS["light_command"])
515 logging.info(f"ํ ํฝ ๊ตฌ๋
: {self.TOPICS['light_command']}")
516
517 # ์จ๋ผ์ธ ์ํ ๋ฐํ
518 client.publish(
519 self.TOPICS["system"],
520 json.dumps({
521 "status": "online",
522 "timestamp": datetime.now().isoformat()
523 }),
524 qos=1,
525 retain=True
526 )
527 else:
528 logging.error(f"MQTT ์ฐ๊ฒฐ ์คํจ (์ฝ๋={rc})")
529
530 def _on_message(self, client, userdata, msg):
531 """MQTT ๋ฉ์์ง ์์ ์ฝ๋ฐฑ"""
532 try:
533 topic = msg.topic
534 payload = json.loads(msg.payload.decode())
535
536 logging.debug(f"MQTT ์์ : {topic} = {payload}")
537
538 # ์กฐ๋ช
๋ช
๋ น ์ฒ๋ฆฌ
539 if "light/command" in topic:
540 self._handle_light_command(topic, payload)
541
542 except json.JSONDecodeError:
543 logging.error(f"์๋ชป๋ JSON: {msg.payload}")
544 except Exception as e:
545 logging.error(f"๋ฉ์์ง ์ฒ๋ฆฌ ์ค๋ฅ: {e}")
546
547 def _handle_light_command(self, topic: str, payload: dict):
548 """
549 ์กฐ๋ช
๋ช
๋ น ์ฒ๋ฆฌ
550
551 Args:
552 topic: MQTT ํ ํฝ (home/{room}/light/command)
553 payload: ๋ช
๋ น ๋ฐ์ดํฐ {"command": "on|off|toggle"}
554 """
555 # ํ ํฝ์์ ๋ฐฉ ID ์ถ์ถ
556 parts = topic.split('/')
557 room = parts[1] if len(parts) >= 2 else None
558
559 if not room:
560 logging.warning(f"๋ฐฉ ID ์์: {topic}")
561 return
562
563 command = payload.get("command")
564
565 # ๋ช
๋ น ์คํ
566 result = False
567 if command == "on":
568 result = self.light_controller.turn_on(room)
569 elif command == "off":
570 result = self.light_controller.turn_off(room)
571 elif command == "toggle":
572 result = self.light_controller.toggle(room)
573 else:
574 logging.warning(f"์ ์ ์๋ ๋ช
๋ น: {command}")
575
576 # ์ํ ๋ฐํ
577 if result:
578 status = self.light_controller.get_status(room)
579 if status:
580 self.publish_light_status(room, status)
581
582 def publish_light_status(self, room: str, status: dict):
583 """
584 ์กฐ๋ช
์ํ ๋ฐํ
585
586 Args:
587 room: ๋ฐฉ ID
588 status: ์ํ ๋ฐ์ดํฐ
589 """
590 if not self.client:
591 return
592
593 topic = self.TOPICS["light_status"].format(room)
594 self.client.publish(topic, json.dumps(status), qos=1, retain=True)
595 logging.debug(f"์กฐ๋ช
์ํ ๋ฐํ: {topic}")
596
597 def publish_sensor_data(self, sensor_id: str, data: dict):
598 """
599 ์ผ์ ๋ฐ์ดํฐ ๋ฐํ
600
601 Args:
602 sensor_id: ์ผ์ ID
603 data: ์ผ์ ๋ฐ์ดํฐ
604 """
605 if not self.client:
606 return
607
608 topic = self.TOPICS["sensor_data"].format(sensor_id)
609 self.client.publish(topic, json.dumps(data), qos=0)
610
611 def publish_motion(self, sensor_id: str, detected: bool):
612 """
613 ๋ชจ์
๊ฐ์ง ๋ฐํ
614
615 Args:
616 sensor_id: ์ผ์ ID
617 detected: ๊ฐ์ง ์ฌ๋ถ
618 """
619 if not self.client:
620 return
621
622 topic = self.TOPICS["motion"].format(sensor_id)
623 data = {
624 "detected": detected,
625 "timestamp": datetime.now().isoformat()
626 }
627 self.client.publish(topic, json.dumps(data), qos=1)
628
629 def publish_automation_event(self, event_type: str, details: dict):
630 """
631 ์๋ํ ์ด๋ฒคํธ ๋ฐํ
632
633 Args:
634 event_type: ์ด๋ฒคํธ ํ์
635 details: ์์ธ ์ ๋ณด
636 """
637 if not self.client:
638 return
639
640 data = {
641 "event_type": event_type,
642 "details": details,
643 "timestamp": datetime.now().isoformat()
644 }
645 self.client.publish(self.TOPICS["automation"], json.dumps(data), qos=1)
646
647 def start(self):
648 """MQTT ๋ฃจํ ์์"""
649 if self.client:
650 self.client.loop_start()
651
652 def stop(self):
653 """MQTT ์ค์ง"""
654 if not self.client:
655 return
656
657 # ์คํ๋ผ์ธ ์ํ ๋ฐํ
658 self.client.publish(
659 self.TOPICS["system"],
660 json.dumps({"status": "offline"}),
661 qos=1,
662 retain=True
663 )
664
665 self.client.loop_stop()
666 self.client.disconnect()
667 logging.info("MQTT ์ฐ๊ฒฐ ์ข
๋ฃ")
668
669
670# ============================================================
671# ์๋ํ ๊ท์น ์์ง
672# ============================================================
673
674class AutomationEngine:
675 """
676 ์๋ํ ๊ท์น ์์ง
677
678 ์ผ์ ๋ฐ์ดํฐ ๊ธฐ๋ฐ์ผ๋ก ์กฐ๋ช
/๊ฐ์ ์ ์๋ ์ ์ดํฉ๋๋ค.
679 """
680
681 def __init__(self, light_controller: LightController, mqtt_handler: SmartHomeMQTT):
682 """
683 ์ด๊ธฐํ
684
685 Args:
686 light_controller: ์กฐ๋ช
์ ์ด๊ธฐ
687 mqtt_handler: MQTT ํธ๋ค๋ฌ
688 """
689 self.light_controller = light_controller
690 self.mqtt_handler = mqtt_handler
691 self.rules: List[dict] = []
692
693 logging.info("AutomationEngine ์ด๊ธฐํ")
694
695 def add_rule(self, name: str, condition: Callable, action: Callable):
696 """
697 ๊ท์น ์ถ๊ฐ
698
699 Args:
700 name: ๊ท์น ์ด๋ฆ
701 condition: ์กฐ๊ฑด ํจ์ (True/False ๋ฐํ)
702 action: ์คํ ํจ์
703 """
704 self.rules.append({
705 "name": name,
706 "condition": condition,
707 "action": action,
708 "last_triggered": None
709 })
710 logging.info(f"์๋ํ ๊ท์น ์ถ๊ฐ: {name}")
711
712 def check_rules(self, sensor_data: dict):
713 """
714 ๊ท์น ๊ฒ์ฌ ๋ฐ ์คํ
715
716 Args:
717 sensor_data: ์ผ์ ๋ฐ์ดํฐ
718 """
719 for rule in self.rules:
720 try:
721 if rule["condition"](sensor_data):
722 # ์กฐ๊ฑด ๋ง์กฑ ์ ์ก์
์คํ
723 logging.info(f"์๋ํ ๊ท์น ํธ๋ฆฌ๊ฑฐ: {rule['name']}")
724 rule["action"](sensor_data)
725 rule["last_triggered"] = datetime.now()
726
727 # MQTT ์ด๋ฒคํธ ๋ฐํ
728 self.mqtt_handler.publish_automation_event(
729 event_type=rule["name"],
730 details=sensor_data
731 )
732
733 except Exception as e:
734 logging.error(f"๊ท์น ์คํ ์ค๋ฅ ({rule['name']}): {e}")
735
736 def get_rules_status(self) -> List[dict]:
737 """๊ท์น ์ํ ์กฐํ"""
738 return [
739 {
740 "name": rule["name"],
741 "last_triggered": rule["last_triggered"].isoformat() if rule["last_triggered"] else None
742 }
743 for rule in self.rules
744 ]
745
746
747# ============================================================
748# ์ค๋งํธํ ๊ฒ์ดํธ์จ์ด (ํตํฉ ์์คํ
)
749# ============================================================
750
751class SmartHomeGateway:
752 """
753 ์ค๋งํธํ ํตํฉ ๊ฒ์ดํธ์จ์ด
754
755 ๋ชจ๋ ์ปดํฌ๋ํธ๋ฅผ ํตํฉํ์ฌ ์ค๋งํธํ ์์คํ
์ ์ด์ํฉ๋๋ค.
756 """
757
758 def __init__(self, config: dict, simulate: bool = True):
759 """
760 ์ด๊ธฐํ
761
762 Args:
763 config: ์ค์ ๋์
๋๋ฆฌ
764 simulate: ์๋ฎฌ๋ ์ด์
๋ชจ๋ ์ฌ๋ถ
765 """
766 self.config = config
767 self.simulate = simulate
768
769 # ์กฐ๋ช
์ ์ด๊ธฐ
770 self.light_controller = LightController(config, simulate=simulate)
771
772 # ํ๊ฒฝ ๋ชจ๋ํฐ
773 self.env_monitor = EnvironmentMonitor(
774 sensor_pin=config.get('dht_pin', 4),
775 sensor_id="env_01",
776 simulate=simulate
777 )
778
779 # MQTT ํธ๋ค๋ฌ
780 self.mqtt_handler = SmartHomeMQTT(
781 self.light_controller,
782 self.env_monitor,
783 broker=config.get('mqtt_broker', 'localhost'),
784 port=config.get('mqtt_port', 1883)
785 )
786
787 # ์๋ํ ์์ง
788 self.automation_engine = AutomationEngine(
789 self.light_controller,
790 self.mqtt_handler
791 )
792
793 # ์ค๋ ๋ ์ ์ด
794 self.running = False
795 self.threads = []
796
797 # ์๋ํ ๊ท์น ์ค์
798 self._setup_automation_rules()
799
800 logging.info("SmartHomeGateway ์ด๊ธฐํ ์๋ฃ")
801
802 def _setup_automation_rules(self):
803 """์๋ํ ๊ท์น ์ค์ """
804
805 # ๊ท์น 1: ์จ๋๊ฐ 30๋ ์ด์์ด๋ฉด ๊ฑฐ์ค ์กฐ๋ช
์ผ๊ธฐ (์: ์์ด์ปจ ๋์ )
806 def temp_high_condition(data):
807 return data.get("temperature", 0) > 30
808
809 def temp_high_action(data):
810 self.light_controller.turn_on("living_room")
811 logging.info(f"[์๋ํ] ๊ณ ์จ ๊ฐ์ง ({data['temperature']}ยฐC) - ๊ฑฐ์ค ์กฐ๋ช
ON")
812
813 self.automation_engine.add_rule(
814 "high_temperature_alert",
815 temp_high_condition,
816 temp_high_action
817 )
818
819 # ๊ท์น 2: ์จ๋๊ฐ 20๋ ์ดํ์ด๋ฉด ๋ชจ๋ ์กฐ๋ช
๋๊ธฐ
820 def temp_low_condition(data):
821 return data.get("temperature", 100) < 20
822
823 def temp_low_action(data):
824 self.light_controller.all_off()
825 logging.info(f"[์๋ํ] ์ ์จ ๊ฐ์ง ({data['temperature']}ยฐC) - ๋ชจ๋ ์กฐ๋ช
OFF")
826
827 self.automation_engine.add_rule(
828 "low_temperature_save",
829 temp_low_condition,
830 temp_low_action
831 )
832
833 # ๊ท์น 3: ์ต๋๊ฐ 80% ์ด์์ด๋ฉด ์์ค ์กฐ๋ช
์ผ๊ธฐ
834 def humidity_high_condition(data):
835 return data.get("humidity", 0) > 80
836
837 def humidity_high_action(data):
838 self.light_controller.turn_on("bathroom")
839 logging.info(f"[์๋ํ] ๊ณ ์ต๋ ๊ฐ์ง ({data['humidity']}%) - ์์ค ์กฐ๋ช
ON")
840
841 self.automation_engine.add_rule(
842 "high_humidity_ventilation",
843 humidity_high_condition,
844 humidity_high_action
845 )
846
847 def _sensor_publish_loop(self, interval: int):
848 """
849 ์ผ์ ๋ฐ์ดํฐ ๋ฐํ ๋ฃจํ
850
851 Args:
852 interval: ๋ฐํ ๊ฐ๊ฒฉ (์ด)
853 """
854 while self.running:
855 data = self.env_monitor.get_latest()
856 if data:
857 # MQTT ๋ฐํ
858 self.mqtt_handler.publish_sensor_data("env_01", data)
859
860 # ์๋ํ ๊ท์น ๊ฒ์ฌ
861 self.automation_engine.check_rules(data)
862
863 time.sleep(interval)
864
865 def _status_report_loop(self, interval: int):
866 """
867 ์ํ ๋ฆฌํฌํธ ๋ฃจํ
868
869 Args:
870 interval: ๋ฆฌํฌํธ ๊ฐ๊ฒฉ (์ด)
871 """
872 while self.running:
873 # ์์คํ
์ํ ์ถ๋ ฅ
874 sensor_data = self.env_monitor.get_latest()
875 light_status = self.light_controller.get_status()
876
877 logging.info("=" * 60)
878 logging.info("์์คํ
์ํ ๋ฆฌํฌํธ")
879 logging.info("-" * 60)
880
881 if sensor_data:
882 logging.info(f"์จ๋: {sensor_data['temperature']}ยฐC, ์ต๋: {sensor_data['humidity']}%")
883
884 if light_status:
885 for light in light_status["lights"]:
886 status = "ON" if light["is_on"] else "OFF"
887 logging.info(f"{light['name']} ({light['location']}): {status}")
888
889 logging.info("=" * 60)
890
891 time.sleep(interval)
892
893 def start(self):
894 """๊ฒ์ดํธ์จ์ด ์์"""
895 if self.running:
896 logging.warning("์ด๋ฏธ ์คํ ์ค์
๋๋ค")
897 return
898
899 logging.info("=" * 60)
900 logging.info("์ค๋งํธํ ๊ฒ์ดํธ์จ์ด ์์")
901 logging.info(f"์๋ฎฌ๋ ์ด์
๋ชจ๋: {self.simulate}")
902 logging.info("=" * 60)
903
904 self.running = True
905
906 # ํ๊ฒฝ ๋ชจ๋ํฐ๋ง ์์
907 self.env_monitor.start(interval=5)
908
909 # MQTT ์ฐ๊ฒฐ ๋ฐ ์์
910 self.mqtt_handler.connect()
911 self.mqtt_handler.start()
912
913 # ์ผ์ ๋ฐ์ดํฐ ๋ฐํ ์ค๋ ๋
914 sensor_thread = threading.Thread(
915 target=self._sensor_publish_loop,
916 args=(10,),
917 daemon=True
918 )
919 sensor_thread.start()
920 self.threads.append(sensor_thread)
921
922 # ์ํ ๋ฆฌํฌํธ ์ค๋ ๋
923 status_thread = threading.Thread(
924 target=self._status_report_loop,
925 args=(30,),
926 daemon=True
927 )
928 status_thread.start()
929 self.threads.append(status_thread)
930
931 logging.info("๊ฒ์ดํธ์จ์ด ์คํ ์ค...")
932
933 def stop(self):
934 """๊ฒ์ดํธ์จ์ด ์ค์ง"""
935 if not self.running:
936 return
937
938 logging.info("๊ฒ์ดํธ์จ์ด ์ค์ง ์ค...")
939
940 self.running = False
941
942 # ์ปดํฌ๋ํธ ์ ๋ฆฌ
943 self.env_monitor.stop()
944 self.mqtt_handler.stop()
945 self.light_controller.all_off()
946 self.light_controller.cleanup()
947
948 # ์ค๋ ๋ ์ข
๋ฃ ๋๊ธฐ
949 for thread in self.threads:
950 thread.join(timeout=2)
951
952 logging.info("๊ฒ์ดํธ์จ์ด ์ค์ง ์๋ฃ")
953
954 def run(self):
955 """๋ฉ์ธ ์คํ ๋ฃจํ"""
956 self.start()
957
958 try:
959 while True:
960 time.sleep(1)
961 except KeyboardInterrupt:
962 logging.info("\nKeyboardInterrupt ์์ ")
963 finally:
964 self.stop()
965
966 def get_dashboard_data(self) -> dict:
967 """
968 ์น ๋์๋ณด๋์ฉ JSON ๋ฐ์ดํฐ ์ ๊ณต
969
970 Returns:
971 ์์คํ
์ ์ฒด ์ํ
972 """
973 return {
974 "timestamp": datetime.now().isoformat(),
975 "lights": self.light_controller.get_status(),
976 "sensor": {
977 "latest": self.env_monitor.get_latest(),
978 "stats": self.env_monitor.get_stats()
979 },
980 "automation": {
981 "rules": self.automation_engine.get_rules_status()
982 },
983 "system": {
984 "running": self.running,
985 "simulation_mode": self.simulate
986 }
987 }
988
989
990# ============================================================
991# ๋ฉ์ธ ์คํ
992# ============================================================
993
994def main():
995 """๋ฉ์ธ ํจ์"""
996
997 # ๋ช
๋ นํ ์ธ์ ํ์ฑ
998 parser = argparse.ArgumentParser(
999 description="์ค๋งํธํ ์๋ํ ์์คํ
",
1000 formatter_class=argparse.RawDescriptionHelpFormatter,
1001 epilog="""
1002์์ :
1003 python home_automation.py --simulate
1004 python home_automation.py --broker mqtt.example.com --simulate
1005 python home_automation.py --loglevel DEBUG --simulate
1006 """
1007 )
1008
1009 parser.add_argument(
1010 "--simulate",
1011 action="store_true",
1012 help="์๋ฎฌ๋ ์ด์
๋ชจ๋ (ํ๋์จ์ด ๋ถํ์)"
1013 )
1014
1015 parser.add_argument(
1016 "--broker",
1017 type=str,
1018 default="localhost",
1019 help="MQTT ๋ธ๋ก์ปค ์ฃผ์ (๊ธฐ๋ณธ๊ฐ: localhost)"
1020 )
1021
1022 parser.add_argument(
1023 "--port",
1024 type=int,
1025 default=1883,
1026 help="MQTT ํฌํธ (๊ธฐ๋ณธ๊ฐ: 1883)"
1027 )
1028
1029 parser.add_argument(
1030 "--loglevel",
1031 type=str,
1032 default="INFO",
1033 choices=["DEBUG", "INFO", "WARNING", "ERROR"],
1034 help="๋ก๊ทธ ๋ ๋ฒจ (๊ธฐ๋ณธ๊ฐ: INFO)"
1035 )
1036
1037 args = parser.parse_args()
1038
1039 # ๋ก๊น
์ค์
1040 logging.basicConfig(
1041 level=getattr(logging, args.loglevel),
1042 format='%(asctime)s [%(levelname)s] %(message)s',
1043 datefmt='%Y-%m-%d %H:%M:%S'
1044 )
1045
1046 # ์๋ฎฌ๋ ์ด์
๋ชจ๋ ์ค์
1047 simulate = args.simulate or not HARDWARE_AVAILABLE
1048
1049 if simulate:
1050 logging.info("์๋ฎฌ๋ ์ด์
๋ชจ๋๋ก ์คํํฉ๋๋ค (ํ๋์จ์ด ๋ถํ์)")
1051 else:
1052 logging.info("์ค์ ํ๋์จ์ด ๋ชจ๋๋ก ์คํํฉ๋๋ค")
1053
1054 # ์ค์
1055 config = {
1056 "lights": [
1057 {
1058 "id": "living_room",
1059 "name": "๊ฑฐ์ค ์กฐ๋ช
",
1060 "gpio_pin": 17,
1061 "location": "๊ฑฐ์ค"
1062 },
1063 {
1064 "id": "bedroom",
1065 "name": "์นจ์ค ์กฐ๋ช
",
1066 "gpio_pin": 27,
1067 "location": "์นจ์ค"
1068 },
1069 {
1070 "id": "kitchen",
1071 "name": "์ฃผ๋ฐฉ ์กฐ๋ช
",
1072 "gpio_pin": 22,
1073 "location": "์ฃผ๋ฐฉ"
1074 },
1075 {
1076 "id": "bathroom",
1077 "name": "์์ค ์กฐ๋ช
",
1078 "gpio_pin": 23,
1079 "location": "์์ค"
1080 }
1081 ],
1082 "dht_pin": 4,
1083 "mqtt_broker": args.broker,
1084 "mqtt_port": args.port
1085 }
1086
1087 # ๊ฒ์ดํธ์จ์ด ์์ฑ ๋ฐ ์คํ
1088 gateway = SmartHomeGateway(config, simulate=simulate)
1089
1090 # ๋ฐ๋ชจ: 5์ด ํ ๋์๋ณด๋ ๋ฐ์ดํฐ ์ถ๋ ฅ
1091 def print_dashboard():
1092 time.sleep(5)
1093 dashboard_data = gateway.get_dashboard_data()
1094 logging.info("\n" + "=" * 60)
1095 logging.info("๋์๋ณด๋ ๋ฐ์ดํฐ (JSON API)")
1096 logging.info("=" * 60)
1097 print(json.dumps(dashboard_data, indent=2, ensure_ascii=False))
1098 logging.info("=" * 60)
1099
1100 demo_thread = threading.Thread(target=print_dashboard, daemon=True)
1101 demo_thread.start()
1102
1103 # ๋ฉ์ธ ๋ฃจํ ์คํ
1104 gateway.run()
1105
1106
1107if __name__ == "__main__":
1108 main()