home_automation.py

Download
python 1109 lines 31.7 KB
   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()