콘텐츠 대표 이미지 - 스마트홈 IoT 제어 소스 완전정복: 조명·전원·센서 연동부터 실전 코드까지
🏠 프로그램/소스

스마트홈 IoT 제어 소스 완전정복: 조명·전원·센서 연동부터 실전 코드까지

💡 직접 만드는 나만의 스마트홈 — 라즈베리파이·아두이노·MQTT·Home Assistant 실전 가이드

야, 솔직히 말해봐. 침대에 누워서 "아 불 끄기 귀찮다..." 이런 생각 한 번쯤 해봤지? 😅
그 귀찮음을 해결하려고 수십만 원짜리 스마트홈 제품 사는 것도 방법이지만, 직접 코드로 만들어버리면 훨씬 재밌고 내 입맛대로 커스터마이징도 가능하거든.

이 글에서는 IoT(Internet of Things) 기반 스마트홈 시스템을 조명 제어, 전원 관리, 센서 연동까지 실제 소스 코드와 함께 낱낱이 파헤쳐볼 거야.
아두이노, 라즈베리파이, ESP8266/ESP32, MQTT 프로토콜, 그리고 Home Assistant까지 — 겁먹지 말고 따라와봐. 생각보다 훨씬 재밌어! 🚀

💡 조명 제어 RGB LED PWM 밝기 조절 🌡️ 센서 연동 온도/습도/PIR DHT22 / HC-SR501 ⚡ 전원 제어 릴레이 모듈 스마트 플러그 ☁️ MQTT 브로커 Mosquitto Home Assistant 🏠 나만의 스마트홈 IoT 시스템

📋

이 글에서 다룰 내용 한눈에 보기

1. 스마트홈 IoT 기초 개념 — 왜 직접 만들어야 하나?
2. 핵심 하드웨어 총정리 (ESP32, 라즈베리파이, 아두이노)
3. 조명 제어 소스 코드 — LED, RGB, 스마트 전구
4. 전원 제어 소스 코드 — 릴레이 모듈, 스마트 플러그
5. 센서 연동 소스 코드 — 온도/습도, PIR, 조도 센서
6. MQTT 프로토콜로 기기 연결하기
7. Home Assistant 연동 설정
8. 실전 통합 예제 — 자동화 시나리오 구현
9. 보안 고려사항 및 팁


🌐

1. 스마트홈 IoT 기초 개념 — 왜 직접 만들어야 하나?

시중에 이미 삼성 SmartThings, 구글 Home, 애플 HomeKit 같은 완성형 스마트홈 플랫폼이 있는데 왜 굳이 직접 만드냐고? 🤔
이유는 간단해. 자유도, 비용, 그리고 재미야.

🏪 완성형 제품 vs 직접 제작 비교

항목 완성형 제품 직접 제작 (DIY)
초기 비용 높음 (기기당 3~10만원) 낮음 (ESP32 기준 3~5천원)
커스터마이징 제한적 무제한
클라우드 의존 높음 (서비스 종료 위험) 로컬 운영 가능
개인정보 기업 서버 전송 내 서버에서만 처리
확장성 브랜드 생태계 제한 원하는 대로 확장

IoT(Internet of Things)는 말 그대로 사물이 인터넷에 연결되는 것이야.
집 안의 조명, 콘센트, 에어컨, 도어락 같은 것들이 네트워크에 연결되어 서로 통신하고 자동화되는 거지.

스마트홈 IoT 시스템의 기본 구조는 이렇게 생겼어:

📡 IoT 스마트홈 기본 아키텍처
1
엣지 디바이스 (Edge Device) — ESP32, 아두이노, 라즈베리파이 등 실제 하드웨어. 센서 데이터를 읽고 액추에이터(조명, 릴레이)를 제어해.
2
통신 프로토콜 — MQTT, HTTP REST API, WebSocket, Zigbee, Z-Wave 등. 기기 간 데이터를 주고받는 언어야.
3
브로커/허브 (Broker/Hub) — MQTT 브로커(Mosquitto), Home Assistant, Node-RED 등. 모든 기기의 중앙 관제탑 역할.
4
사용자 인터페이스 (UI) — 스마트폰 앱, 웹 대시보드, 음성 어시스턴트. 사람이 시스템을 제어하는 창구.
💡 핵심 포인트: 이 글에서는 주로 ESP32 + MQTT + Home Assistant 조합을 중심으로 설명할 거야. 이 조합이 현재 DIY 스마트홈에서 가장 인기 있고 커뮤니티도 활발하거든!

🔧

2. 핵심 하드웨어 총정리

코드 짜기 전에 하드웨어부터 알아야 해. 어떤 보드를 쓰느냐에 따라 코드 스타일도 달라지거든. 🛠️

▶ESP8266 / ESP32

DIY 스마트홈의 국민 보드야. 가격이 저렴하고(ESP32 기준 3,000~6,000원), Wi-Fi가 내장되어 있어서 별도 모듈 없이 바로 인터넷에 연결할 수 있어.
ESP32는 ESP8266의 업그레이드 버전으로 블루투스까지 지원하고, 듀얼 코어 CPU에 더 많은 GPIO 핀을 가지고 있어.

ESP32 주요 스펙

항목스펙
CPUXtensa LX6 듀얼코어 240MHz
RAM520KB SRAM
플래시4MB (모델에 따라 다름)
Wi-Fi802.11 b/g/n (2.4GHz)
블루투스BT 4.2 + BLE
GPIO34핀
ADC12비트 ADC 18채널
PWM16채널

▶라즈베리파이 (Raspberry Pi)

스마트홈의 두뇌 역할을 하는 싱글보드 컴퓨터야. 리눅스가 돌아가기 때문에 Home Assistant, MQTT 브로커, Node-RED 같은 서버 소프트웨어를 직접 돌릴 수 있어.
라즈베리파이 4 기준으로 4GB RAM 모델이 스마트홈 허브로 가장 많이 쓰여.

▶아두이노 (Arduino)

입문자에게 친숙한 보드야. 하지만 기본 아두이노 우노는 Wi-Fi가 없어서 스마트홈에 쓰려면 ESP8266 쉴드나 아두이노 MKR WiFi 1010 같은 Wi-Fi 지원 모델을 써야 해.
요즘은 ESP32가 아두이노 IDE에서도 개발 가능하기 때문에, 아두이노 문법 그대로 ESP32를 쓰는 경우가 많아.

💡 추천 조합: - 허브/서버: 라즈베리파이 4 (Home Assistant + Mosquitto MQTT 브로커)
- 엣지 디바이스: ESP32 (조명, 전원, 센서 제어)
- 개발 환경: Arduino IDE 또는 PlatformIO (VS Code 확장)

▶주요 부품 목록

부품용도대략 가격
ESP32 개발보드메인 컨트롤러3,000~6,000원
5V 릴레이 모듈전원 ON/OFF 제어500~1,500원
DHT22 센서온도/습도 측정2,000~4,000원
HC-SR501 PIR움직임 감지500~1,500원
BH1750 조도센서밝기 측정1,000~2,500원
WS2812B LED 스트립RGB 조명5,000~15,000원
MOSFET (IRLZ44N)LED 밝기 PWM 제어200~500원
라즈베리파이 4 (4GB)스마트홈 허브70,000~90,000원

💡

3. 조명 제어 소스 코드 — LED, RGB, 스마트 전구

자, 이제 진짜 코드 얘기야! 🎉
조명 제어는 스마트홈의 가장 기본이자 가장 많이 쓰이는 기능이야. 크게 세 가지로 나눌 수 있어:

조명 제어 방식 3가지
1
단순 ON/OFF — 릴레이나 트랜지스터로 전원을 끊거나 연결. 가장 단순한 방식.
2
PWM 밝기 조절 — MOSFET + PWM 신호로 LED 밝기를 0~100%로 조절.
3
RGB 컬러 제어 — WS2812B 같은 어드레서블 LED로 색상과 밝기를 개별 제어.

▶기본 LED ON/OFF 제어 (ESP32 + MQTT)

가장 기본적인 예제야. ESP32가 MQTT 브로커에서 메시지를 받아서 LED를 켜고 끄는 코드야.


// ESP32 LED ON/OFF 제어 - MQTT 연동
// 필요 라이브러리: PubSubClient, WiFi

#include <WiFi.h>
#include <PubSubClient.h>

// ===== 설정값 =====
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
const char* mqtt_server = "192.168.1.100";  // 라즈베리파이 IP
const int mqtt_port = 1883;
const char* mqtt_client_id = "esp32_light_01";

// MQTT 토픽 정의
const char* topic_light_cmd = "home/livingroom/light/set";    // 명령 수신
const char* topic_light_state = "home/livingroom/light/state"; // 상태 발행

// GPIO 핀 설정
const int LED_PIN = 2;  // ESP32 내장 LED 또는 외부 LED

WiFiClient espClient;
PubSubClient client(espClient);

// ===== Wi-Fi 연결 함수 =====
void setupWiFi() {
  Serial.print("Wi-Fi 연결 중: ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);
  
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  
  Serial.println("\nWi-Fi 연결 완료!");
  Serial.print("IP 주소: ");
  Serial.println(WiFi.localIP());
}

// ===== MQTT 메시지 수신 콜백 =====
void mqttCallback(char* topic, byte* payload, unsigned int length) {
  // 수신된 메시지를 문자열로 변환
  String message = "";
  for (int i = 0; i < length; i++) {
    message += (char)payload[i];
  }
  
  Serial.print("메시지 수신 [");
  Serial.print(topic);
  Serial.print("]: ");
  Serial.println(message);
  
  // 조명 제어 토픽 처리
  if (String(topic) == topic_light_cmd) {
    if (message == "ON" || message == "1" || message == "true") {
      digitalWrite(LED_PIN, HIGH);
      client.publish(topic_light_state, "ON");  // 상태 발행
      Serial.println("💡 조명 ON");
    } 
    else if (message == "OFF" || message == "0" || message == "false") {
      digitalWrite(LED_PIN, LOW);
      client.publish(topic_light_state, "OFF");  // 상태 발행
      Serial.println("🌑 조명 OFF");
    }
  }
}

// ===== MQTT 재연결 함수 =====
void reconnectMQTT() {
  while (!client.connected()) {
    Serial.print("MQTT 연결 시도...");
    
    if (client.connect(mqtt_client_id)) {
      Serial.println("연결 성공!");
      // 토픽 구독
      client.subscribe(topic_light_cmd);
      Serial.print("구독 토픽: ");
      Serial.println(topic_light_cmd);
      
      // 연결 시 현재 상태 발행
      int currentState = digitalRead(LED_PIN);
      client.publish(topic_light_state, currentState ? "ON" : "OFF");
    } else {
      Serial.print("연결 실패, rc=");
      Serial.print(client.state());
      Serial.println(" 5초 후 재시도...");
      delay(5000);
    }
  }
}

// ===== setup() =====
void setup() {
  Serial.begin(115200);
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, LOW);  // 초기 상태: OFF
  
  setupWiFi();
  
  client.setServer(mqtt_server, mqtt_port);
  client.setCallback(mqttCallback);
}

// ===== loop() =====
void loop() {
  if (!client.connected()) {
    reconnectMQTT();
  }
  client.loop();  // MQTT 메시지 처리
}
💡 PubSubClient 라이브러리 설치: Arduino IDE에서 스케치 → 라이브러리 포함하기 → 라이브러리 관리에서 "PubSubClient" 검색 후 설치해줘. Nick O'Leary가 만든 버전이야.

▶PWM 밝기 조절 제어

단순 ON/OFF를 넘어서 밝기를 0~255 단계로 조절하는 코드야.
MQTT 메시지로 밝기 값(0~255)을 받아서 PWM으로 LED 밝기를 제어해.


// ESP32 PWM 밝기 조절 - MQTT 연동
// JSON 형식 메시지 처리 포함
// 필요 라이브러리: PubSubClient, WiFi, ArduinoJson

#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>

const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
const char* mqtt_server = "192.168.1.100";

// MQTT 토픽
const char* topic_light_cmd = "home/bedroom/light/set";
const char* topic_light_state = "home/bedroom/light/state";

// PWM 설정 (ESP32 LEDC)
const int LED_PIN = 18;
const int PWM_CHANNEL = 0;
const int PWM_FREQ = 5000;    // 5kHz
const int PWM_RESOLUTION = 8; // 8비트 = 0~255

int currentBrightness = 0;
bool lightState = false;

WiFiClient espClient;
PubSubClient client(espClient);

// 상태 JSON 발행 함수
void publishState() {
  StaticJsonDocument<128> doc;
  doc["state"] = lightState ? "ON" : "OFF";
  doc["brightness"] = currentBrightness;
  
  char buffer[128];
  serializeJson(doc, buffer);
  client.publish(topic_light_state, buffer, true); // retain=true
  
  Serial.print("상태 발행: ");
  Serial.println(buffer);
}

// MQTT 콜백
void mqttCallback(char* topic, byte* payload, unsigned int length) {
  String message = "";
  for (int i = 0; i < length; i++) {
    message += (char)payload[i];
  }
  
  Serial.print("수신: ");
  Serial.println(message);
  
  // JSON 파싱
  StaticJsonDocument<128> doc;
  DeserializationError error = deserializeJson(doc, message);
  
  if (!error) {
    // JSON 형식: {"state": "ON", "brightness": 128}
    if (doc.containsKey("state")) {
      String state = doc["state"].as<String>();
      lightState = (state == "ON");
    }
    
    if (doc.containsKey("brightness")) {
      currentBrightness = doc["brightness"].as<int>();
      currentBrightness = constrain(currentBrightness, 0, 255);
    }
  } else {
    // 단순 문자열 처리
    if (message == "ON") {
      lightState = true;
      if (currentBrightness == 0) currentBrightness = 255;
    } else if (message == "OFF") {
      lightState = false;
    }
  }
  
  // PWM 출력 적용
  int pwmValue = lightState ? currentBrightness : 0;
  ledcWrite(PWM_CHANNEL, pwmValue);
  
  Serial.print("PWM 출력: ");
  Serial.println(pwmValue);
  
  publishState();
}

void reconnectMQTT() {
  while (!client.connected()) {
    if (client.connect("esp32_bedroom_light")) {
      client.subscribe(topic_light_cmd);
      publishState();
    } else {
      delay(5000);
    }
  }
}

void setup() {
  Serial.begin(115200);
  
  // ESP32 LEDC PWM 설정
  ledcSetup(PWM_CHANNEL, PWM_FREQ, PWM_RESOLUTION);
  ledcAttachPin(LED_PIN, PWM_CHANNEL);
  ledcWrite(PWM_CHANNEL, 0);  // 초기값 0 (OFF)
  
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) delay(500);
  
  client.setServer(mqtt_server, 1883);
  client.setCallback(mqttCallback);
}

void loop() {
  if (!client.connected()) reconnectMQTT();
  client.loop();
}
⚠️ ESP32 PWM 주의사항: ESP32는 아두이노의 analogWrite()를 지원하지 않아! 대신 ledcSetup(), ledcAttachPin(), ledcWrite()를 사용해야 해. ESP32 Arduino Core 3.x 버전부터는 API가 약간 바뀌었으니 버전 확인 필수!

▶WS2812B RGB LED 스트립 제어

이제 진짜 화려한 거야! 🌈 WS2812B는 각 LED를 개별적으로 제어할 수 있는 어드레서블 LED야.
FastLED 라이브러리를 사용하면 엄청 쉽게 제어할 수 있어.


// ESP32 WS2812B RGB LED 스트립 제어 - MQTT 연동
// 필요 라이브러리: PubSubClient, WiFi, FastLED, ArduinoJson

#include <WiFi.h>
#include <PubSubClient.h>
#include <FastLED.h>
#include <ArduinoJson.h>

const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
const char* mqtt_server = "192.168.1.100";

// LED 스트립 설정
#define LED_PIN     5
#define NUM_LEDS    60    // LED 개수 (1m 기준 60개)
#define LED_TYPE    WS2812B
#define COLOR_ORDER GRB   // WS2812B는 GRB 순서!

CRGB leds[NUM_LEDS];

// MQTT 토픽
const char* topic_rgb_cmd = "home/livingroom/rgb/set";
const char* topic_rgb_state = "home/livingroom/rgb/state";

// 현재 상태
bool rgbState = false;
int brightness = 128;
int r = 255, g = 255, b = 255;  // 기본 흰색
String effect = "solid";  // 효과: solid, rainbow, pulse

WiFiClient espClient;
PubSubClient client(espClient);

// 효과 변수
unsigned long lastEffectUpdate = 0;
int hue = 0;

// 무지개 효과
void rainbowEffect() {
  if (millis() - lastEffectUpdate > 20) {
    for (int i = 0; i < NUM_LEDS; i++) {
      leds[i] = CHSV(hue + (i * 256 / NUM_LEDS), 255, brightness);
    }
    FastLED.show();
    hue++;
    lastEffectUpdate = millis();
  }
}

// 펄스 효과
void pulseEffect() {
  static int pulseVal = 0;
  static int pulseDir = 1;
  
  if (millis() - lastEffectUpdate > 15) {
    pulseVal += pulseDir * 3;
    if (pulseVal >= 255) { pulseVal = 255; pulseDir = -1; }
    if (pulseVal <= 0)   { pulseVal = 0;   pulseDir = 1;  }
    
    fill_solid(leds, NUM_LEDS, CRGB(
      map(pulseVal, 0, 255, 0, r),
      map(pulseVal, 0, 255, 0, g),
      map(pulseVal, 0, 255, 0, b)
    ));
    FastLED.show();
    lastEffectUpdate = millis();
  }
}

// 상태 발행
void publishRGBState() {
  StaticJsonDocument<256> doc;
  doc["state"] = rgbState ? "ON" : "OFF";
  doc["brightness"] = brightness;
  
  JsonObject color = doc.createNestedObject("color");
  color["r"] = r;
  color["g"] = g;
  color["b"] = b;
  
  doc["effect"] = effect;
  
  char buffer[256];
  serializeJson(doc, buffer);
  client.publish(topic_rgb_state, buffer, true);
}

// MQTT 콜백
void mqttCallback(char* topic, byte* payload, unsigned int length) {
  String message = "";
  for (int i = 0; i < length; i++) {
    message += (char)payload[i];
  }
  
  StaticJsonDocument<256> doc;
  if (deserializeJson(doc, message)) return;
  
  // 상태 처리
  if (doc.containsKey("state")) {
    rgbState = (String(doc["state"].as<const char*>()) == "ON");
  }
  
  // 밝기 처리
  if (doc.containsKey("brightness")) {
    brightness = constrain(doc["brightness"].as<int>(), 0, 255);
    FastLED.setBrightness(brightness);
  }
  
  // 색상 처리
  if (doc.containsKey("color")) {
    r = doc["color"]["r"] | 255;
    g = doc["color"]["g"] | 255;
    b = doc["color"]["b"] | 255;
  }
  
  // 효과 처리
  if (doc.containsKey("effect")) {
    effect = doc["effect"].as<String>();
  }
  
  // 즉시 적용 (solid 효과)
  if (!rgbState) {
    fill_solid(leds, NUM_LEDS, CRGB::Black);
    FastLED.show();
  } else if (effect == "solid") {
    fill_solid(leds, NUM_LEDS, CRGB(r, g, b));
    FastLED.show();
  }
  
  publishRGBState();
}

void reconnectMQTT() {
  while (!client.connected()) {
    if (client.connect("esp32_rgb_strip")) {
      client.subscribe(topic_rgb_cmd);
      publishRGBState();
    } else {
      delay(5000);
    }
  }
}

void setup() {
  Serial.begin(115200);
  
  // FastLED 초기화
  FastLED.addLeds<LED_TYPE, LED_PIN, COLOR_ORDER>(leds, NUM_LEDS);
  FastLED.setBrightness(brightness);
  fill_solid(leds, NUM_LEDS, CRGB::Black);
  FastLED.show();
  
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) delay(500);
  
  client.setServer(mqtt_server, 1883);
  client.setCallback(mqttCallback);
}

void loop() {
  if (!client.connected()) reconnectMQTT();
  client.loop();
  
  // 효과 처리 (loop에서 지속 실행)
  if (rgbState) {
    if (effect == "rainbow") rainbowEffect();
    else if (effect == "pulse") pulseEffect();
  }
}
💡 WS2812B 배선 팁: - 데이터 라인에 330Ω 저항을 직렬로 연결해줘 (신호 보호)
- LED 스트립 전원은 5V 2A 이상 별도 전원 공급 (60개 기준 최대 3.6A 소비)
- ESP32와 LED 스트립의 GND를 공통 연결해야 해!

⚡

4. 전원 제어 소스 코드 — 릴레이 모듈, 스마트 플러그

조명 다음은 전원 제어야. 릴레이 모듈을 사용하면 220V AC 전원도 ESP32로 제어할 수 있어.
물론 고전압 작업이니까 안전에 각별히 주의해야 해! ⚠️

⚠️ 안전 경고: 220V AC 전원을 다루는 작업은 감전 위험이 있어. 반드시 전원을 차단한 상태에서 배선하고, 절연 처리를 철저히 해줘. 자신이 없다면 전기 기사에게 의뢰하거나, 기성품 스마트 플러그를 사용하는 게 안전해.

▶릴레이 모듈 기본 제어

릴레이는 전자석을 이용해서 기계적 스위치를 ON/OFF하는 부품이야.
5V 릴레이 모듈은 ESP32의 3.3V GPIO로도 제어 가능한 모델을 사용해야 해 (로직 레벨 변환 내장 모델).


// ESP32 릴레이 제어 - 다중 채널 + MQTT
// 4채널 릴레이 모듈 예제

#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>

const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
const char* mqtt_server = "192.168.1.100";

// 릴레이 핀 설정 (4채널)
const int RELAY_PINS[] = {26, 27, 14, 12};
const int RELAY_COUNT = 4;

// 릴레이 이름 (MQTT 토픽용)
const char* RELAY_NAMES[] = {
  "outlet1",   // 콘센트 1
  "outlet2",   // 콘센트 2  
  "fan",       // 선풍기
  "heater"     // 히터
};

// 릴레이 상태 저장
bool relayStates[4] = {false, false, false, false};

// 릴레이 타입: true = 활성화 HIGH, false = 활성화 LOW (모듈에 따라 다름)
// 대부분의 중국산 릴레이 모듈은 LOW 활성화!
const bool RELAY_ACTIVE_HIGH = false;

WiFiClient espClient;
PubSubClient client(espClient);

// 릴레이 제어 함수
void setRelay(int index, bool state) {
  if (index < 0 || index >= RELAY_COUNT) return;
  
  relayStates[index] = state;
  
  // 활성화 레벨에 따른 출력
  if (RELAY_ACTIVE_HIGH) {
    digitalWrite(RELAY_PINS[index], state ? HIGH : LOW);
  } else {
    // LOW 활성화 (대부분의 릴레이 모듈)
    digitalWrite(RELAY_PINS[index], state ? LOW : HIGH);
  }
  
  // 상태 발행
  char topic[64];
  snprintf(topic, sizeof(topic), "home/power/%s/state", RELAY_NAMES[index]);
  client.publish(topic, state ? "ON" : "OFF", true);
  
  Serial.printf("릴레이 %d (%s): %s\n", 
    index + 1, RELAY_NAMES[index], state ? "ON" : "OFF");
}

// MQTT 콜백
void mqttCallback(char* topic, byte* payload, unsigned int length) {
  String topicStr = String(topic);
  String message = "";
  for (int i = 0; i < length; i++) {
    message += (char)payload[i];
  }
  
  // 토픽 파싱: home/power/{name}/set
  for (int i = 0; i < RELAY_COUNT; i++) {
    char expectedTopic[64];
    snprintf(expectedTopic, sizeof(expectedTopic), 
             "home/power/%s/set", RELAY_NAMES[i]);
    
    if (topicStr == expectedTopic) {
      if (message == "ON" || message == "1") {
        setRelay(i, true);
      } else if (message == "OFF" || message == "0") {
        setRelay(i, false);
      } else if (message == "TOGGLE") {
        setRelay(i, !relayStates[i]);
      }
      break;
    }
  }
  
  // 전체 제어: home/power/all/set
  if (topicStr == "home/power/all/set") {
    bool state = (message == "ON");
    for (int i = 0; i < RELAY_COUNT; i++) {
      setRelay(i, state);
    }
  }
}

// 타이머 기반 자동 OFF 기능
struct RelayTimer {
  bool active;
  unsigned long offTime;
};
RelayTimer timers[4] = {{false,0},{false,0},{false,0},{false,0}};

void setRelayTimer(int index, int seconds) {
  timers[index].active = true;
  timers[index].offTime = millis() + (seconds * 1000UL);
  setRelay(index, true);
  Serial.printf("릴레이 %d: %d초 후 자동 OFF\n", index+1, seconds);
}

void checkTimers() {
  for (int i = 0; i < RELAY_COUNT; i++) {
    if (timers[i].active && millis() >= timers[i].offTime) {
      timers[i].active = false;
      setRelay(i, false);
      Serial.printf("릴레이 %d: 타이머 OFF\n", i+1);
    }
  }
}

void reconnectMQTT() {
  while (!client.connected()) {
    if (client.connect("esp32_relay_controller")) {
      // 모든 릴레이 토픽 구독
      for (int i = 0; i < RELAY_COUNT; i++) {
        char topic[64];
        snprintf(topic, sizeof(topic), "home/power/%s/set", RELAY_NAMES[i]);
        client.subscribe(topic);
      }
      client.subscribe("home/power/all/set");
      
      // 현재 상태 발행
      for (int i = 0; i < RELAY_COUNT; i++) {
        char topic[64];
        snprintf(topic, sizeof(topic), "home/power/%s/state", RELAY_NAMES[i]);
        client.publish(topic, relayStates[i] ? "ON" : "OFF", true);
      }
    } else {
      delay(5000);
    }
  }
}

void setup() {
  Serial.begin(115200);
  
  // 릴레이 핀 초기화
  for (int i = 0; i < RELAY_COUNT; i++) {
    pinMode(RELAY_PINS[i], OUTPUT);
    // LOW 활성화 릴레이는 초기값 HIGH (OFF 상태)
    digitalWrite(RELAY_PINS[i], RELAY_ACTIVE_HIGH ? LOW : HIGH);
    relayStates[i] = false;
  }
  
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) delay(500);
  Serial.println("Wi-Fi 연결 완료");
  
  client.setServer(mqtt_server, 1883);
  client.setCallback(mqttCallback);
}

void loop() {
  if (!client.connected()) reconnectMQTT();
  client.loop();
  checkTimers();  // 타이머 체크
}
💡 릴레이 모듈 선택 팁: - 광커플러(Optocoupler) 내장 모델을 선택해. ESP32 GPIO를 고전압 회로로부터 보호해줘.
- SSR(Solid State Relay)은 기계식 릴레이보다 수명이 길고 소음이 없어. 고빈도 스위칭에 적합.
- 릴레이 코일 역기전력 보호를 위해 플라이백 다이오드를 연결하는 게 좋아 (대부분 모듈에 내장).

▶Tasmota 펌웨어 활용 (스마트 플러그)

직접 코드를 짜는 것 외에도, Tasmota라는 오픈소스 펌웨어를 ESP8266/ESP32 기반 스마트 플러그에 플래싱해서 사용하는 방법도 있어.
Tasmota는 MQTT를 기본 지원하고, Home Assistant와 자동 연동되는 강력한 펌웨어야.

Tasmota MQTT 명령어 예시

명령토픽페이로드
전원 ONcmnd/tasmota_XXXXXX/PowerON
전원 OFFcmnd/tasmota_XXXXXX/PowerOFF
토글cmnd/tasmota_XXXXXX/PowerTOGGLE
상태 조회cmnd/tasmota_XXXXXX/Status0
에너지 모니터링cmnd/tasmota_XXXXXX/Status8

Tasmota가 발행하는 상태 메시지 예시:


// Tasmota 상태 메시지 예시 (JSON)
// 토픽: stat/tasmota_XXXXXX/POWER

{
  "Time": "2024-01-15T14:30:00",
  "POWER": "ON"
}

// 에너지 모니터링 메시지 (전력 측정 모델)
// 토픽: tele/tasmota_XXXXXX/SENSOR

{
  "Time": "2024-01-15T14:30:10",
  "ENERGY": {
    "TotalStartTime": "2024-01-01T00:00:00",
    "Total": 12.345,        // 총 소비 전력량 (kWh)
    "Yesterday": 0.234,     // 어제 소비량 (kWh)
    "Today": 0.089,         // 오늘 소비량 (kWh)
    "Period": 0,
    "Power": 45,            // 현재 소비 전력 (W)
    "ApparentPower": 48,
    "ReactivePower": 16,
    "Factor": 0.94,         // 역률
    "Voltage": 220,         // 전압 (V)
    "Current": 0.218        // 전류 (A)
  }
}

🌡️

5. 센서 연동 소스 코드 — 온도/습도, PIR, 조도 센서

센서는 스마트홈의 눈과 귀야. 센서 데이터를 기반으로 자동화 규칙을 만들 수 있거든.
"온도가 28도 이상이면 에어컨 켜기", "움직임 감지되면 조명 켜기" 같은 것들이 다 센서 덕분에 가능해! 🎯

▶DHT22 온도/습도 센서

DHT22는 가장 많이 쓰이는 온습도 센서야. DHT11보다 정확도가 높고 측정 범위도 넓어.
(DHT11: 온도 0~50°C, 습도 20~80% / DHT22: 온도 -40~80°C, 습도 0~100%)


// ESP32 DHT22 온습도 센서 - MQTT 발행
// 필요 라이브러리: PubSubClient, WiFi, DHT sensor library, ArduinoJson

#include <WiFi.h>
#include <PubSubClient.h>
#include <DHT.h>
#include <ArduinoJson.h>

const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
const char* mqtt_server = "192.168.1.100";

// DHT 설정
#define DHT_PIN  4
#define DHT_TYPE DHT22
DHT dht(DHT_PIN, DHT_TYPE);

// MQTT 토픽
const char* topic_sensor = "home/sensor/livingroom/climate";
const char* topic_temp   = "home/sensor/livingroom/temperature";
const char* topic_humi   = "home/sensor/livingroom/humidity";

// 발행 주기 (30초)
const unsigned long PUBLISH_INTERVAL = 30000;
unsigned long lastPublish = 0;

WiFiClient espClient;
PubSubClient client(espClient);

// 센서 데이터 읽기 및 발행
void readAndPublishSensor() {
  // DHT22는 읽기 실패 시 NaN 반환
  float temperature = dht.readTemperature();
  float humidity = dht.readHumidity();
  
  // 유효성 검사
  if (isnan(temperature) || isnan(humidity)) {
    Serial.println("DHT22 읽기 실패!");
    return;
  }
  
  // 열지수(Heat Index) 계산
  float heatIndex = dht.computeHeatIndex(temperature, humidity, false);
  
  Serial.printf("온도: %.1f°C, 습도: %.1f%%, 열지수: %.1f°C\n", 
    temperature, humidity, heatIndex);
  
  // JSON 형식으로 발행 (통합 토픽)
  StaticJsonDocument<256> doc;
  doc["temperature"] = round(temperature * 10) / 10.0;  // 소수점 1자리
  doc["humidity"] = round(humidity * 10) / 10.0;
  doc["heat_index"] = round(heatIndex * 10) / 10.0;
  doc["unit"] = "celsius";
  
  char buffer[256];
  serializeJson(doc, buffer);
  client.publish(topic_sensor, buffer, true);
  
  // 개별 토픽으로도 발행 (Home Assistant 자동 발견용)
  char tempStr[8], humiStr[8];
  dtostrf(temperature, 4, 1, tempStr);
  dtostrf(humidity, 4, 1, humiStr);
  client.publish(topic_temp, tempStr, true);
  client.publish(topic_humi, humiStr, true);
}

void reconnectMQTT() {
  while (!client.connected()) {
    if (client.connect("esp32_climate_sensor")) {
      Serial.println("MQTT 연결 완료");
      readAndPublishSensor();  // 연결 즉시 발행
    } else {
      delay(5000);
    }
  }
}

void setup() {
  Serial.begin(115200);
  dht.begin();
  
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) delay(500);
  
  client.setServer(mqtt_server, 1883);
}

void loop() {
  if (!client.connected()) reconnectMQTT();
  client.loop();
  
  // 주기적 발행
  if (millis() - lastPublish >= PUBLISH_INTERVAL) {
    lastPublish = millis();
    readAndPublishSensor();
  }
}

▶PIR 움직임 감지 센서 (HC-SR501)

PIR(Passive Infrared) 센서는 사람의 체온(적외선)을 감지해서 움직임을 탐지해.
현관이나 복도에 설치해서 "사람이 오면 조명 켜기" 같은 자동화에 딱이야!


// ESP32 PIR 움직임 감지 - MQTT 연동
// HC-SR501 센서 사용

#include <WiFi.h>
#include <PubSubClient.h>

const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
const char* mqtt_server = "192.168.1.100";

// PIR 핀 설정
const int PIR_PIN = 13;

// MQTT 토픽
const char* topic_motion = "home/sensor/hallway/motion";
const char* topic_motion_count = "home/sensor/hallway/motion_count";

// 상태 변수
bool lastMotionState = false;
int motionCount = 0;
unsigned long lastMotionTime = 0;
const unsigned long MOTION_COOLDOWN = 5000;  // 5초 쿨다운

// 인터럽트 플래그
volatile bool motionDetected = false;

WiFiClient espClient;
PubSubClient client(espClient);

// PIR 인터럽트 핸들러
void IRAM_ATTR onMotionDetected() {
  motionDetected = true;
}

void publishMotionState(bool state) {
  client.publish(topic_motion, state ? "ON" : "OFF", true);
  
  if (state) {
    motionCount++;
    char countStr[8];
    itoa(motionCount, countStr, 10);
    client.publish(topic_motion_count, countStr, true);
    
    Serial.printf("🚶 움직임 감지! (총 %d회)\n", motionCount);
  } else {
    Serial.println("😴 움직임 없음");
  }
}

void reconnectMQTT() {
  while (!client.connected()) {
    if (client.connect("esp32_pir_sensor")) {
      client.publish(topic_motion, "OFF", true);
    } else {
      delay(5000);
    }
  }
}

void setup() {
  Serial.begin(115200);
  
  // PIR 핀 설정 (인터럽트 사용)
  pinMode(PIR_PIN, INPUT);
  attachInterrupt(digitalPinToInterrupt(PIR_PIN), onMotionDetected, CHANGE);
  
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) delay(500);
  
  client.setServer(mqtt_server, 1883);
  
  // HC-SR501 워밍업 대기 (약 60초)
  Serial.println("PIR 센서 워밍업 중... (60초)");
  delay(60000);
  Serial.println("준비 완료!");
}

void loop() {
  if (!client.connected()) reconnectMQTT();
  client.loop();
  
  // 인터럽트 플래그 처리
  if (motionDetected) {
    motionDetected = false;
    
    bool currentState = digitalRead(PIR_PIN);
    
    // 상태 변화 감지
    if (currentState != lastMotionState) {
      // 쿨다운 체크 (연속 감지 방지)
      if (currentState || (millis() - lastMotionTime > MOTION_COOLDOWN)) {
        lastMotionState = currentState;
        if (currentState) lastMotionTime = millis();
        publishMotionState(currentState);
      }
    }
  }
}

▶BH1750 조도 센서

조도 센서는 주변 밝기를 측정해서 "어두워지면 조명 켜기" 같은 자동화에 사용해.
BH1750은 I2C 통신을 사용하는 디지털 조도 센서야. 측정 범위는 1~65535 lux.


// ESP32 BH1750 조도 센서 - MQTT 연동
// I2C 통신 사용
// 필요 라이브러리: BH1750, PubSubClient, WiFi

#include <WiFi.h>
#include <PubSubClient.h>
#include <Wire.h>
#include <BH1750.h>

const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
const char* mqtt_server = "192.168.1.100";

// I2C 핀 (ESP32 기본: SDA=21, SCL=22)
const int SDA_PIN = 21;
const int SCL_PIN = 22;

BH1750 lightMeter;

// MQTT 토픽
const char* topic_lux = "home/sensor/livingroom/illuminance";
const char* topic_lux_state = "home/sensor/livingroom/light_state";

// 조도 임계값 (자동화 기준)
const float LUX_DARK = 50.0;    // 50 lux 이하: 어두움
const float LUX_DIM = 200.0;    // 200 lux 이하: 흐림
const float LUX_BRIGHT = 500.0; // 500 lux 이상: 밝음

const unsigned long PUBLISH_INTERVAL = 10000;  // 10초
unsigned long lastPublish = 0;

WiFiClient espClient;
PubSubClient client(espClient);

String getLightState(float lux) {
  if (lux < LUX_DARK) return "dark";
  else if (lux < LUX_DIM) return "dim";
  else if (lux < LUX_BRIGHT) return "normal";
  else return "bright";
}

void readAndPublishLux() {
  float lux = lightMeter.readLightLevel();
  
  if (lux < 0) {
    Serial.println("BH1750 읽기 실패!");
    return;
  }
  
  String lightState = getLightState(lux);
  
  Serial.printf("조도: %.1f lux (%s)\n", lux, lightState.c_str());
  
  // 조도값 발행
  char luxStr[10];
  dtostrf(lux, 5, 1, luxStr);
  client.publish(topic_lux, luxStr, true);
  
  // 조도 상태 발행
  client.publish(topic_lux_state, lightState.c_str(), true);
}

void reconnectMQTT() {
  while (!client.connected()) {
    if (client.connect("esp32_light_sensor")) {
      readAndPublishLux();
    } else {
      delay(5000);
    }
  }
}

void setup() {
  Serial.begin(115200);
  
  Wire.begin(SDA_PIN, SCL_PIN);
  
  if (!lightMeter.begin(BH1750::CONTINUOUS_HIGH_RES_MODE)) {
    Serial.println("BH1750 초기화 실패!");
    while (1);
  }
  Serial.println("BH1750 초기화 완료");
  
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) delay(500);
  
  client.setServer(mqtt_server, 1883);
}

void loop() {
  if (!client.connected()) reconnectMQTT();
  client.loop();
  
  if (millis() - lastPublish >= PUBLISH_INTERVAL) {
    lastPublish = millis();
    readAndPublishLux();
  }
}
💡 다중 센서 통합 팁: 여러 센서를 하나의 ESP32에 연결할 때는 I2C 버스를 공유할 수 있어. BH1750(I2C), BME280(I2C)을 같은 SDA/SCL 핀에 연결하고 주소만 다르게 설정하면 돼. DHT22는 별도 GPIO 핀을 사용해.

MQTT 기반 스마트홈 데이터 흐름 ☁️ MQTT 브로커 Mosquitto (라즈베리파이) 포트: 1883 (일반) / 8883 (TLS) 🌡️ ESP32 센서 온도/습도/조도 PUBLISH → 💡 ESP32 조명 RGB LED 스트립 ← SUBSCRIBE ⚡ ESP32 전원 릴레이 4채널 ← SUBSCRIBE 🏠 Home Assistant 자동화 / 대시보드 ← SUBSCRIBE / PUBLISH → 📱 스마트폰 앱 MQTT Dashboard ← SUBSCRIBE / PUBLISH → 🔄 Node-RED 자동화 플로우 ← SUBSCRIBE / PUBLISH → home/sensor/livingroom/temperature home/livingroom/light/set home/power/outlet1/state ← MQTT 토픽 구조 예시 모든 기기가 MQTT 브로커를 통해 메시지를 주고받아 — Pub/Sub 패턴

📡

6. MQTT 프로토콜로 기기 연결하기

MQTT(Message Queuing Telemetry Transport)는 IoT에서 가장 많이 쓰이는 경량 메시지 프로토콜이야.
HTTP보다 훨씬 가볍고, 저전력 기기에서도 잘 동작해. 발행(Publish)/구독(Subscribe) 패턴을 사용해. 📨

▶Mosquitto MQTT 브로커 설치 (라즈베리파이)


# 라즈베리파이에 Mosquitto 설치
sudo apt update
sudo apt install -y mosquitto mosquitto-clients

# 서비스 시작 및 자동 시작 설정
sudo systemctl start mosquitto
sudo systemctl enable mosquitto

# 상태 확인
sudo systemctl status mosquitto

# /etc/mosquitto/mosquitto.conf 설정 파일 편집
sudo nano /etc/mosquitto/conf.d/default.conf

# 아래 내용 추가:
listener 1883
allow_anonymous true  # 개발용 (운영 시 반드시 인증 설정!)

# 로그 설정
log_dest file /var/log/mosquitto/mosquitto.log
log_type all

# 저장 후 재시작
sudo systemctl restart mosquitto

# 테스트: 터미널 1 - 구독
mosquitto_sub -h localhost -t "home/#" -v

# 테스트: 터미널 2 - 발행
mosquitto_pub -h localhost -t "home/test" -m "Hello IoT!"

# 조명 제어 테스트
mosquitto_pub -h 192.168.1.100 -t "home/livingroom/light/set" -m "ON"
mosquitto_pub -h 192.168.1.100 -t "home/livingroom/rgb/set" \
  -m '{"state":"ON","brightness":200,"color":{"r":255,"g":100,"b":50}}'

▶MQTT 토픽 설계 규칙

토픽 구조를 잘 설계해야 나중에 관리하기 편해. 일반적으로 이런 구조를 따라:

📂 추천 MQTT 토픽 구조

패턴예시설명
home/{방}/{기기}/sethome/bedroom/light/set제어 명령 (구독)
home/{방}/{기기}/statehome/bedroom/light/state현재 상태 (발행)
home/sensor/{방}/{타입}home/sensor/kitchen/temperature센서 데이터
home/status/{기기ID}home/status/esp32_01기기 온라인 상태

▶Last Will and Testament (LWT) — 기기 오프라인 감지

LWT는 기기가 예기치 않게 연결이 끊겼을 때 브로커가 자동으로 메시지를 발행하는 기능이야.
기기가 오프라인인지 온라인인지 실시간으로 알 수 있어.


// LWT (Last Will and Testament) 설정 예제
// ESP32가 갑자기 연결이 끊기면 브로커가 자동으로 "offline" 메시지 발행

#include <WiFi.h>
#include <PubSubClient.h>

const char* mqtt_server = "192.168.1.100";
const char* device_id = "esp32_livingroom_01";

// LWT 토픽 및 메시지
char lwt_topic[64];
const char* lwt_message = "offline";
const char* online_message = "online";

WiFiClient espClient;
PubSubClient client(espClient);

void reconnectMQTT() {
  // LWT 토픽 생성
  snprintf(lwt_topic, sizeof(lwt_topic), "home/status/%s", device_id);
  
  while (!client.connected()) {
    // connect() 함수에 LWT 파라미터 추가
    // connect(clientId, username, password, willTopic, willQos, willRetain, willMessage)
    if (client.connect(
          device_id,          // 클라이언트 ID
          NULL,               // 사용자명 (없으면 NULL)
          NULL,               // 비밀번호 (없으면 NULL)
          lwt_topic,          // LWT 토픽
          1,                  // QoS 1
          true,               // retain
          lwt_message         // LWT 메시지
        )) {
      
      // 연결 성공 시 온라인 상태 발행
      client.publish(lwt_topic, online_message, true);
      Serial.println("MQTT 연결 완료 - 온라인 상태 발행");
      
      // 토픽 구독
      client.subscribe("home/livingroom/light/set");
      
    } else {
      delay(5000);
    }
  }
}

void setup() {
  Serial.begin(115200);
  WiFi.begin("YOUR_SSID", "YOUR_PASSWORD");
  while (WiFi.status() != WL_CONNECTED) delay(500);
  
  client.setServer(mqtt_server, 1883);
  // 콜백 설정...
}

void loop() {
  if (!client.connected()) reconnectMQTT();
  client.loop();
}
💡 MQTT QoS 레벨 이해: - QoS 0: 최대 1회 전달 (전달 보장 없음, 가장 빠름)
- QoS 1: 최소 1회 전달 (중복 가능, 일반적으로 사용)
- QoS 2: 정확히 1회 전달 (가장 안전하지만 느림)
스마트홈에서는 보통 QoS 1을 사용해. 센서 데이터는 QoS 0도 OK.

🏠

7. Home Assistant 연동 설정

Home Assistant(HA)는 오픈소스 스마트홈 플랫폼의 끝판왕이야. 🏆
라즈베리파이에 설치하면 수천 가지 기기와 서비스를 통합 관리할 수 있어.
MQTT Discovery 기능을 사용하면 ESP32 기기가 자동으로 HA에 등록돼!

▶Home Assistant 설치 (라즈베리파이)


# Home Assistant OS 설치 방법 (권장)
# 1. https://www.home-assistant.io/installation/ 에서 라즈베리파이용 이미지 다운로드
# 2. Raspberry Pi Imager로 SD카드에 플래싱
# 3. 라즈베리파이 부팅 후 http://homeassistant.local:8123 접속

# 또는 Docker로 설치 (기존 라즈베리파이 OS에서)
docker run -d \
  --name homeassistant \
  --privileged \
  --restart=unless-stopped \
  -e TZ=Asia/Seoul \
  -v /home/pi/homeassistant:/config \
  --network=host \
  ghcr.io/home-assistant/home-assistant:stable

▶MQTT 통합 설정 (configuration.yaml)

HA에서 MQTT 기기를 수동으로 등록하는 방법이야.
configuration.yaml 파일에 기기 정보를 추가해줘.


# /config/configuration.yaml

# MQTT 브로커 설정 (HA UI에서도 설정 가능)
mqtt:
  broker: 192.168.1.100
  port: 1883
  username: ""
  password: ""

# 조명 엔티티 등록
light:
  # 단순 ON/OFF 조명
  - platform: mqtt
    name: "거실 조명"
    unique_id: "livingroom_light_01"
    state_topic: "home/livingroom/light/state"
    command_topic: "home/livingroom/light/set"
    payload_on: "ON"
    payload_off: "OFF"
    retain: true
    
  # 밝기 조절 가능한 조명
  - platform: mqtt
    name: "침실 조명"
    unique_id: "bedroom_light_01"
    state_topic: "home/bedroom/light/state"
    command_topic: "home/bedroom/light/set"
    brightness_state_topic: "home/bedroom/light/state"
    brightness_command_topic: "home/bedroom/light/set"
    brightness_value_template: "{{ value_json.brightness }}"
    state_value_template: "{{ value_json.state }}"
    on_command_type: "brightness"
    retain: true
    
  # RGB LED 스트립
  - platform: mqtt
    name: "거실 RGB 스트립"
    unique_id: "livingroom_rgb_01"
    state_topic: "home/livingroom/rgb/state"
    command_topic: "home/livingroom/rgb/set"
    state_value_template: "{{ value_json.state }}"
    brightness_state_topic: "home/livingroom/rgb/state"
    brightness_value_template: "{{ value_json.brightness }}"
    rgb_state_topic: "home/livingroom/rgb/state"
    rgb_value_template: >
      {{ value_json.color.r }},
      {{ value_json.color.g }},
      {{ value_json.color.b }}
    effect_state_topic: "home/livingroom/rgb/state"
    effect_value_template: "{{ value_json.effect }}"
    effect_list:
      - solid
      - rainbow
      - pulse
    retain: true

# 스위치 (전원 제어)
switch:
  - platform: mqtt
    name: "콘센트 1"
    unique_id: "outlet_01"
    state_topic: "home/power/outlet1/state"
    command_topic: "home/power/outlet1/set"
    payload_on: "ON"
    payload_off: "OFF"
    retain: true
    
  - platform: mqtt
    name: "선풍기"
    unique_id: "fan_01"
    state_topic: "home/power/fan/state"
    command_topic: "home/power/fan/set"
    payload_on: "ON"
    payload_off: "OFF"
    retain: true

# 센서 등록
sensor:
  - platform: mqtt
    name: "거실 온도"
    unique_id: "livingroom_temperature"
    state_topic: "home/sensor/livingroom/temperature"
    unit_of_measurement: "°C"
    device_class: temperature
    value_template: "{{ value | float | round(1) }}"
    
  - platform: mqtt
    name: "거실 습도"
    unique_id: "livingroom_humidity"
    state_topic: "home/sensor/livingroom/humidity"
    unit_of_measurement: "%"
    device_class: humidity
    value_template: "{{ value | float | round(1) }}"
    
  - platform: mqtt
    name: "거실 조도"
    unique_id: "livingroom_illuminance"
    state_topic: "home/sensor/livingroom/illuminance"
    unit_of_measurement: "lx"
    device_class: illuminance

# 이진 센서 (PIR 움직임 감지)
binary_sensor:
  - platform: mqtt
    name: "복도 움직임"
    unique_id: "hallway_motion"
    state_topic: "home/sensor/hallway/motion"
    payload_on: "ON"
    payload_off: "OFF"
    device_class: motion

▶MQTT Discovery — 자동 기기 등록

수동으로 configuration.yaml을 편집하는 대신, ESP32에서 특정 형식의 메시지를 발행하면 HA가 자동으로 기기를 인식하는 MQTT Discovery 기능을 사용할 수 있어!


// ESP32 MQTT Discovery 자동 등록 예제
// Home Assistant가 자동으로 기기를 인식함

void publishDiscovery() {
  // 조명 Discovery 메시지
  // 토픽: homeassistant/light/{unique_id}/config
  
  StaticJsonDocument<512> lightDoc;
  lightDoc["name"] = "거실 조명";
  lightDoc["unique_id"] = "esp32_livingroom_light";
  lightDoc["state_topic"] = "home/livingroom/light/state";
  lightDoc["command_topic"] = "home/livingroom/light/set";
  lightDoc["brightness_state_topic"] = "home/livingroom/light/state";
  lightDoc["brightness_command_topic"] = "home/livingroom/light/set";
  lightDoc["brightness_value_template"] = "{{ value_json.brightness }}";
  lightDoc["state_value_template"] = "{{ value_json.state }}";
  lightDoc["payload_on"] = "ON";
  lightDoc["payload_off"] = "OFF";
  
  // 기기 정보 (같은 기기의 여러 엔티티를 묶어줌)
  JsonObject device = lightDoc.createNestedObject("device");
  device["identifiers"][0] = "esp32_livingroom_01";
  device["name"] = "거실 ESP32";
  device["model"] = "ESP32 DevKit";
  device["manufacturer"] = "DIY";
  device["sw_version"] = "1.0.0";
  
  char buffer[512];
  serializeJson(lightDoc, buffer);
  
  // retain=true 로 발행 (HA 재시작 후에도 유지)
  client.publish(
    "homeassistant/light/esp32_livingroom_light/config",
    buffer,
    true  // retain
  );
  
  Serial.println("Discovery 메시지 발행 완료!");
  
  // 센서 Discovery 메시지
  StaticJsonDocument<512> sensorDoc;
  sensorDoc["name"] = "거실 온도";
  sensorDoc["unique_id"] = "esp32_livingroom_temp";
  sensorDoc["state_topic"] = "home/sensor/livingroom/temperature";
  sensorDoc["unit_of_measurement"] = "°C";
  sensorDoc["device_class"] = "temperature";
  sensorDoc["value_template"] = "{{ value | float | round(1) }}";
  
  JsonObject sensorDevice = sensorDoc.createNestedObject("device");
  sensorDevice["identifiers"][0] = "esp32_livingroom_01";
  sensorDevice["name"] = "거실 ESP32";
  
  char sensorBuffer[512];
  serializeJson(sensorDoc, sensorBuffer);
  
  client.publish(
    "homeassistant/sensor/esp32_livingroom_temp/config",
    sensorBuffer,
    true
  );
}
💡 MQTT Discovery 토픽 구조: homeassistant/{component}/{object_id}/config
- component: light, switch, sensor, binary_sensor, climate 등
- object_id: 고유한 식별자
- 이 토픽으로 JSON 설정을 발행하면 HA가 자동으로 엔티티를 생성해!

▶Home Assistant 자동화 예제

HA의 자동화 기능으로 센서 데이터에 따라 기기를 자동 제어할 수 있어.


# /config/automations.yaml

# 자동화 1: 움직임 감지 시 복도 조명 켜기
- id: "motion_light_hallway"
  alias: "복도 움직임 감지 조명 자동화"
  description: "PIR 센서 감지 시 복도 조명 켜고, 5분 후 자동 끄기"
  trigger:
    - platform: state
      entity_id: binary_sensor.복도_움직임
      to: "on"
  action:
    - service: light.turn_on
      target:
        entity_id: light.복도_조명
      data:
        brightness: 200
    - delay: "00:05:00"  # 5분 대기
    - service: light.turn_off
      target:
        entity_id: light.복도_조명

# 자동화 2: 온도 기반 에어컨 제어
- id: "temp_aircon_control"
  alias: "온도 기반 에어컨 자동 제어"
  trigger:
    - platform: numeric_state
      entity_id: sensor.거실_온도
      above: 28
  condition:
    - condition: time
      after: "09:00:00"
      before: "23:00:00"
  action:
    - service: switch.turn_on
      target:
        entity_id: switch.에어컨

# 자동화 3: 일몰 시 거실 조명 켜기
- id: "sunset_light"
  alias: "일몰 시 거실 조명 자동 켜기"
  trigger:
    - platform: sun
      event: sunset
      offset: "-00:30:00"  # 일몰 30분 전
  condition:
    - condition: state
      entity_id: binary_sensor.거실_움직임
      state: "on"
  action:
    - service: light.turn_on
      target:
        entity_id: light.거실_rgb_스트립
      data:
        brightness: 150
        rgb_color: [255, 200, 100]  # 따뜻한 노란빛
        effect: "solid"

# 자동화 4: 조도 기반 조명 자동 제어
- id: "lux_auto_light"
  alias: "조도 낮을 때 조명 자동 켜기"
  trigger:
    - platform: state
      entity_id: sensor.거실_조도_상태
      to: "dark"
  condition:
    - condition: time
      after: "07:00:00"
      before: "22:00:00"
  action:
    - service: light.turn_on
      target:
        entity_id: light.거실_조명

🚀

8. 실전 통합 예제 — 자동화 시나리오 구현

자, 이제 모든 걸 합쳐서 완전한 스마트홈 노드를 만들어보자! 🎯
하나의 ESP32에 온습도 센서, PIR 센서, 조명 제어, 릴레이를 모두 통합한 예제야.
이걸 기반으로 각 방에 하나씩 설치하면 완성형 스마트홈이 돼!

🔌 통합 노드 하드웨어 구성

핀연결 부품기능
GPIO 4DHT22온도/습도 측정
GPIO 13HC-SR501 PIR움직임 감지
GPIO 18MOSFET → LEDPWM 밝기 조절
GPIO 5WS2812B 데이터RGB LED 스트립
GPIO 26릴레이 1전원 제어 1
GPIO 27릴레이 2전원 제어 2
GPIO 21BH1750 SDAI2C 데이터
GPIO 22BH1750 SCLI2C 클럭

// ===================================================
// 스마트홈 통합 노드 - ESP32 완전 예제
// 조명 + 전원 + 센서 + MQTT + Home Assistant Discovery
// ===================================================

#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include <DHT.h>
#include <FastLED.h>
#include <Wire.h>
#include <BH1750.h>

// ===== 설정 =====
const char* WIFI_SSID = "YOUR_WIFI_SSID";
const char* WIFI_PASS = "YOUR_WIFI_PASSWORD";
const char* MQTT_SERVER = "192.168.1.100";
const int   MQTT_PORT = 1883;
const char* DEVICE_ID = "esp32_room01";
const char* ROOM_NAME = "bedroom";  // 방 이름

// ===== 핀 설정 =====
#define DHT_PIN     4
#define PIR_PIN     13
#define DIM_PIN     18   // PWM 조명
#define RGB_PIN     5    // WS2812B
#define RELAY1_PIN  26
#define RELAY2_PIN  27
#define SDA_PIN     21
#define SCL_PIN     22

// ===== LED 설정 =====
#define NUM_LEDS    30
#define DHT_TYPE    DHT22
#define PWM_CH      0
#define PWM_FREQ    5000
#define PWM_RES     8

CRGB leds[NUM_LEDS];
DHT dht(DHT_PIN, DHT_TYPE);
BH1750 lightMeter;

WiFiClient espClient;
PubSubClient mqtt(espClient);

// ===== 상태 변수 =====
struct DeviceState {
  // 조명
  bool dimLight = false;
  int dimBrightness = 128;
  bool rgbLight = false;
  int rgbBrightness = 128;
  int rgbR = 255, rgbG = 255, rgbB = 255;
  String rgbEffect = "solid";
  
  // 전원
  bool relay1 = false;
  bool relay2 = false;
  
  // 센서
  float temperature = 0;
  float humidity = 0;
  float lux = 0;
  bool motion = false;
} state;

// ===== MQTT 토픽 생성 헬퍼 =====
String topic(String type, String device, String action) {
  return "home/" + String(ROOM_NAME) + "/" + type + "/" + device + "/" + action;
}

// ===== 상태 발행 함수들 =====
void publishDimLight() {
  StaticJsonDocument<128> doc;
  doc["state"] = state.dimLight ? "ON" : "OFF";
  doc["brightness"] = state.dimBrightness;
  char buf[128]; serializeJson(doc, buf);
  mqtt.publish(topic("light","dim","state").c_str(), buf, true);
}

void publishRGBLight() {
  StaticJsonDocument<256> doc;
  doc["state"] = state.rgbLight ? "ON" : "OFF";
  doc["brightness"] = state.rgbBrightness;
  JsonObject color = doc.createNestedObject("color");
  color["r"] = state.rgbR;
  color["g"] = state.rgbG;
  color["b"] = state.rgbB;
  doc["effect"] = state.rgbEffect;
  char buf[256]; serializeJson(doc, buf);
  mqtt.publish(topic("light","rgb","state").c_str(), buf, true);
}

void publishRelays() {
  mqtt.publish(topic("power","relay1","state").c_str(), 
               state.relay1 ? "ON" : "OFF", true);
  mqtt.publish(topic("power","relay2","state").c_str(), 
               state.relay2 ? "ON" : "OFF", true);
}

void publishSensors() {
  char buf[10];
  
  dtostrf(state.temperature, 4, 1, buf);
  mqtt.publish(topic("sensor","temperature","state").c_str(), buf, true);
  
  dtostrf(state.humidity, 4, 1, buf);
  mqtt.publish(topic("sensor","humidity","state").c_str(), buf, true);
  
  dtostrf(state.lux, 6, 1, buf);
  mqtt.publish(topic("sensor","illuminance","state").c_str(), buf, true);
  
  mqtt.publish(topic("sensor","motion","state").c_str(), 
               state.motion ? "ON" : "OFF", true);
}

// ===== 하드웨어 제어 함수들 =====
void applyDimLight() {
  int pwm = state.dimLight ? state.dimBrightness : 0;
  ledcWrite(PWM_CH, pwm);
}

void applyRGBLight() {
  if (!state.rgbLight) {
    fill_solid(leds, NUM_LEDS, CRGB::Black);
    FastLED.show();
    return;
  }
  FastLED.setBrightness(state.rgbBrightness);
  if (state.rgbEffect == "solid") {
    fill_solid(leds, NUM_LEDS, CRGB(state.rgbR, state.rgbG, state.rgbB));
    FastLED.show();
  }
  // rainbow, pulse 효과는 loop()에서 처리
}

void applyRelay(int pin, bool state_val) {
  digitalWrite(pin, state_val ? LOW : HIGH);  // LOW 활성화
}

// ===== MQTT 콜백 =====
void mqttCallback(char* topicChar, byte* payload, unsigned int length) {
  String topicStr = String(topicChar);
  String msg = "";
  for (int i = 0; i < length; i++) msg += (char)payload[i];
  
  // 조명 제어
  if (topicStr == topic("light","dim","set")) {
    StaticJsonDocument<128> doc;
    if (!deserializeJson(doc, msg)) {
      if (doc.containsKey("state")) 
        state.dimLight = (String(doc["state"].as<const char*>()) == "ON");
      if (doc.containsKey("brightness")) 
        state.dimBrightness = constrain(doc["brightness"].as<int>(), 0, 255);
    } else {
      state.dimLight = (msg == "ON");
    }
    applyDimLight();
    publishDimLight();
  }
  
  // RGB 제어
  else if (topicStr == topic("light","rgb","set")) {
    StaticJsonDocument<256> doc;
    if (!deserializeJson(doc, msg)) {
      if (doc.containsKey("state"))
        state.rgbLight = (String(doc["state"].as<const char*>()) == "ON");
      if (doc.containsKey("brightness"))
        state.rgbBrightness = constrain(doc["brightness"].as<int>(), 0, 255);
      if (doc.containsKey("color")) {
        state.rgbR = doc["color"]["r"] | 255;
        state.rgbG = doc["color"]["g"] | 255;
        state.rgbB = doc["color"]["b"] | 255;
      }
      if (doc.containsKey("effect"))
        state.rgbEffect = doc["effect"].as<String>();
    }
    applyRGBLight();
    publishRGBLight();
  }
  
  // 릴레이 제어
  else if (topicStr == topic("power","relay1","set")) {
    state.relay1 = (msg == "ON");
    applyRelay(RELAY1_PIN, state.relay1);
    publishRelays();
  }
  else if (topicStr == topic("power","relay2","set")) {
    state.relay2 = (msg == "ON");
    applyRelay(RELAY2_PIN, state.relay2);
    publishRelays();
  }
}

// ===== MQTT Discovery 발행 =====
void publishDiscovery() {
  // 기기 정보 (공통)
  auto addDevice = [](JsonObject& obj) {
    JsonArray ids = obj.createNestedArray("identifiers");
    ids.add(DEVICE_ID);
    obj["name"] = String("방 스마트노드 (") + ROOM_NAME + ")";
    obj["model"] = "ESP32 SmartNode v1.0";
    obj["manufacturer"] = "DIY SmartHome";
  };
  
  // 조명 Discovery
  {
    StaticJsonDocument<512> doc;
    doc["name"] = String(ROOM_NAME) + " 조명";
    doc["unique_id"] = String(DEVICE_ID) + "_dim";
    doc["state_topic"] = topic("light","dim","state");
    doc["command_topic"] = topic("light","dim","set");
    doc["state_value_template"] = "{{ value_json.state }}";
    doc["brightness_state_topic"] = topic("light","dim","state");
    doc["brightness_value_template"] = "{{ value_json.brightness }}";
    JsonObject dev = doc.createNestedObject("device");
    addDevice(dev);
    char buf[512]; serializeJson(doc, buf);
    String discTopic = "homeassistant/light/" + String(DEVICE_ID) + "_dim/config";
    mqtt.publish(discTopic.c_str(), buf, true);
  }
  
  // 온도 센서 Discovery
  {
    StaticJsonDocument<384> doc;
    doc["name"] = String(ROOM_NAME) + " 온도";
    doc["unique_id"] = String(DEVICE_ID) + "_temp";
    doc["state_topic"] = topic("sensor","temperature","state");
    doc["unit_of_measurement"] = "°C";
    doc["device_class"] = "temperature";
    JsonObject dev = doc.createNestedObject("device");
    addDevice(dev);
    char buf[384]; serializeJson(doc, buf);
    String discTopic = "homeassistant/sensor/" + String(DEVICE_ID) + "_temp/config";
    mqtt.publish(discTopic.c_str(), buf, true);
  }
  
  Serial.println("Discovery 발행 완료!");
}

// ===== MQTT 재연결 =====
void reconnectMQTT() {
  char lwtTopic[64];
  snprintf(lwtTopic, sizeof(lwtTopic), "home/status/%s", DEVICE_ID);
  
  while (!mqtt.connected()) {
    Serial.print("MQTT 연결 중...");
    if (mqtt.connect(DEVICE_ID, NULL, NULL, lwtTopic, 1, true, "offline")) {
      Serial.println("연결 완료!");
      mqtt.publish(lwtTopic, "online", true);
      
      // 토픽 구독
      mqtt.subscribe(topic("light","dim","set").c_str());
      mqtt.subscribe(topic("light","rgb","set").c_str());
      mqtt.subscribe(topic("power","relay1","set").c_str());
      mqtt.subscribe(topic("power","relay2","set").c_str());
      
      // Discovery 발행
      publishDiscovery();
      
      // 현재 상태 발행
      publishDimLight();
      publishRGBLight();
      publishRelays();
      publishSensors();
    } else {
      Serial.printf("실패 (rc=%d), 5초 후 재시도\n", mqtt.state());
      delay(5000);
    }
  }
}

// ===== 센서 읽기 =====
unsigned long lastSensorRead = 0;
const unsigned long SENSOR_INTERVAL = 30000;

void readSensors() {
  if (millis() - lastSensorRead < SENSOR_INTERVAL) return;
  lastSensorRead = millis();
  
  float t = dht.readTemperature();
  float h = dht.readHumidity();
  if (!isnan(t) && !isnan(h)) {
    state.temperature = t;
    state.humidity = h;
  }
  
  float l = lightMeter.readLightLevel();
  if (l >= 0) state.lux = l;
  
  bool m = digitalRead(PIR_PIN);
  if (m != state.motion) {
    state.motion = m;
    mqtt.publish(topic("sensor","motion","state").c_str(), m ? "ON" : "OFF", true);
  }
  
  publishSensors();
}

// ===== 효과 처리 =====
unsigned long lastEffect = 0;
int effectHue = 0;
int pulseVal = 0, pulseDir = 1;

void handleEffects() {
  if (!state.rgbLight) return;
  if (millis() - lastEffect < 20) return;
  lastEffect = millis();
  
  if (state.rgbEffect == "rainbow") {
    for (int i = 0; i < NUM_LEDS; i++)
      leds[i] = CHSV(effectHue + (i * 256 / NUM_LEDS), 255, state.rgbBrightness);
    FastLED.show();
    effectHue++;
  } else if (state.rgbEffect == "pulse") {
    pulseVal += pulseDir * 3;
    if (pulseVal >= 255) { pulseVal = 255; pulseDir = -1; }
    if (pulseVal <= 0)   { pulseVal = 0;   pulseDir = 1;  }
    fill_solid(leds, NUM_LEDS, CRGB(
      map(pulseVal, 0, 255, 0, state.rgbR),
      map(pulseVal, 0, 255, 0, state.rgbG),
      map(pulseVal, 0, 255, 0, state.rgbB)
    ));
    FastLED.show();
  }
}

// ===== setup() =====
void setup() {
  Serial.begin(115200);
  Serial.println("\n🏠 스마트홈 노드 시작!");
  
  // 핀 초기화
  pinMode(PIR_PIN, INPUT);
  pinMode(RELAY1_PIN, OUTPUT); digitalWrite(RELAY1_PIN, HIGH);
  pinMode(RELAY2_PIN, OUTPUT); digitalWrite(RELAY2_PIN, HIGH);
  
  // PWM 초기화
  ledcSetup(PWM_CH, PWM_FREQ, PWM_RES);
  ledcAttachPin(DIM_PIN, PWM_CH);
  ledcWrite(PWM_CH, 0);
  
  // FastLED 초기화
  FastLED.addLeds<WS2812B, RGB_PIN, GRB>(leds, NUM_LEDS);
  FastLED.setBrightness(0);
  fill_solid(leds, NUM_LEDS, CRGB::Black);
  FastLED.show();
  
  // DHT 초기화
  dht.begin();
  
  // I2C + BH1750 초기화
  Wire.begin(SDA_PIN, SCL_PIN);
  lightMeter.begin(BH1750::CONTINUOUS_HIGH_RES_MODE);
  
  // Wi-Fi 연결
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  Serial.print("Wi-Fi 연결 중");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500); Serial.print(".");
  }
  Serial.printf("\nIP: %s\n", WiFi.localIP().toString().c_str());
  
  // MQTT 설정
  mqtt.setServer(MQTT_SERVER, MQTT_PORT);
  mqtt.setCallback(mqttCallback);
  mqtt.setBufferSize(512);  // 버퍼 크기 증가 (Discovery 메시지용)
}

// ===== loop() =====
void loop() {
  if (!mqtt.connected()) reconnectMQTT();
  mqtt.loop();
  
  readSensors();    // 센서 읽기
  handleEffects();  // LED 효과 처리
}
💡 재능넷에서도 이런 IoT 프로젝트 관련 재능을 거래할 수 있어! 스마트홈 구축 도움이 필요하거나, 반대로 내가 만든 IoT 솔루션을 공유하고 싶다면 재능넷을 활용해봐. 하드웨어 설계부터 소프트웨어 개발까지 다양한 전문가들이 활동하고 있거든. 😊

🔒

9. 보안 고려사항 및 실전 팁

스마트홈을 만들었으면 보안도 신경 써야 해. 보안이 허술하면 해커가 집 안 기기를 마음대로 제어할 수도 있거든! 😱

▶MQTT 인증 설정


# Mosquitto 인증 설정

# 1. 비밀번호 파일 생성
sudo mosquitto_passwd -c /etc/mosquitto/passwd iot_user
# 비밀번호 입력 프롬프트가 나타남

# 2. /etc/mosquitto/conf.d/default.conf 수정
listener 1883
allow_anonymous false           # 익명 접속 차단
password_file /etc/mosquitto/passwd  # 비밀번호 파일 지정

# 3. TLS 암호화 설정 (선택사항, 외부 접속 시 필수)
listener 8883
cafile /etc/mosquitto/certs/ca.crt
certfile /etc/mosquitto/certs/server.crt
keyfile /etc/mosquitto/certs/server.key
require_certificate false

# 재시작
sudo systemctl restart mosquitto

// ESP32에서 MQTT 인증 사용
// connect() 함수에 사용자명/비밀번호 추가

const char* mqtt_user = "iot_user";
const char* mqtt_pass = "your_secure_password";

// reconnectMQTT() 함수에서:
if (client.connect(device_id, mqtt_user, mqtt_pass, lwtTopic, 1, true, "offline")) {
  // 연결 성공
}

▶Wi-Fi 보안 팁

🛡️ IoT 보안 체크리스트
1
IoT 전용 Wi-Fi 네트워크 분리 — 공유기에서 게스트 네트워크나 VLAN을 만들어서 IoT 기기를 메인 네트워크와 분리해. 기기가 해킹당해도 PC나 스마트폰에 영향이 없어.
2
MQTT 인증 필수 — 절대 allow_anonymous true로 운영하지 마. 특히 외부에서 접속 가능한 경우에는 TLS 암호화도 적용해.
3
펌웨어 OTA 업데이트 — ESP32는 OTA(Over-The-Air) 업데이트를 지원해. 보안 취약점이 발견되면 원격으로 펌웨어를 업데이트할 수 있어.
4
하드코딩된 자격증명 금지 — Wi-Fi 비밀번호, MQTT 비밀번호를 코드에 직접 쓰지 말고, ESP32의 NVS(Non-Volatile Storage)나 설정 파일로 관리해.
5
로컬 우선 운영 — 가능하면 클라우드 없이 로컬 네트워크에서만 동작하도록 설계해. 외부 서비스 의존도를 줄이면 보안과 안정성이 올라가.

▶ESP32 OTA 업데이트 코드


// ESP32 OTA (Over-The-Air) 업데이트 설정
// 네트워크를 통해 펌웨어를 원격 업데이트

#include <WiFi.h>
#include <ArduinoOTA.h>

void setupOTA() {
  ArduinoOTA.setHostname("esp32-smartnode-01");
  ArduinoOTA.setPassword("ota_password_here");  // OTA 비밀번호
  
  ArduinoOTA.onStart([]() {
    String type = (ArduinoOTA.getCommand() == U_FLASH) ? "스케치" : "파일시스템";
    Serial.println("OTA 시작: " + type);
  });
  
  ArduinoOTA.onEnd([]() {
    Serial.println("\nOTA 완료! 재시작...");
  });
  
  ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
    Serial.printf("진행: %u%%\r", (progress / (total / 100)));
  });
  
  ArduinoOTA.onError([](ota_error_t error) {
    Serial.printf("OTA 오류[%u]: ", error);
    if (error == OTA_AUTH_ERROR) Serial.println("인증 실패");
    else if (error == OTA_BEGIN_ERROR) Serial.println("시작 실패");
    else if (error == OTA_CONNECT_ERROR) Serial.println("연결 실패");
    else if (error == OTA_RECEIVE_ERROR) Serial.println("수신 실패");
    else if (error == OTA_END_ERROR) Serial.println("종료 실패");
  });
  
  ArduinoOTA.begin();
  Serial.println("OTA 준비 완료");
}

void setup() {
  Serial.begin(115200);
  WiFi.begin("YOUR_SSID", "YOUR_PASS");
  while (WiFi.status() != WL_CONNECTED) delay(500);
  
  setupOTA();
  // ... 나머지 초기화
}

void loop() {
  ArduinoOTA.handle();  // OTA 처리 (반드시 loop에 포함!)
  // ... 나머지 코드
}

▶WiFiManager — 설정 파일 없이 Wi-Fi 설정

코드에 Wi-Fi 비밀번호를 하드코딩하는 대신, WiFiManager 라이브러리를 사용하면 스마트폰으로 Wi-Fi 설정을 할 수 있어.


// WiFiManager 사용 예제
// 처음 실행 시 AP 모드로 동작, 스마트폰으로 접속해서 Wi-Fi 설정
// 필요 라이브러리: WiFiManager by tzapu

#include <WiFiManager.h>

void setup() {
  Serial.begin(115200);
  
  WiFiManager wm;
  
  // 커스텀 파라미터 추가 (MQTT 서버 주소 등)
  WiFiManagerParameter mqtt_server("mqtt", "MQTT 서버 IP", "192.168.1.100", 16);
  wm.addParameter(&mqtt_server);
  
  // 설정 포털 타임아웃 (180초)
  wm.setConfigPortalTimeout(180);
  
  // 자동 연결 시도, 실패 시 "SmartHome-Setup" AP 생성
  if (!wm.autoConnect("SmartHome-Setup", "setup1234")) {
    Serial.println("Wi-Fi 연결 실패, 재시작...");
    ESP.restart();
  }
  
  Serial.println("Wi-Fi 연결 완료!");
  Serial.print("MQTT 서버: ");
  Serial.println(mqtt_server.getValue());
  
  // MQTT 서버 주소를 NVS에 저장
  // preferences.putString("mqtt_server", mqtt_server.getValue());
}
💡 Node-RED 활용 팁: Home Assistant 외에도 Node-RED를 함께 사용하면 더 복잡한 자동화 플로우를 시각적으로 만들 수 있어. 라즈베리파이에 Node-RED를 설치하고 MQTT 노드를 연결하면, 드래그앤드롭으로 자동화 로직을 구성할 수 있거든. 코딩 없이도 복잡한 조건 분기가 가능해!

🎓

10. 트러블슈팅 & 자주 묻는 질문

실제로 만들다 보면 이런저런 문제가 생기기 마련이야. 자주 발생하는 문제들을 정리해봤어. 🔧

❓ Q: ESP32가 MQTT에 연결됐다가 자꾸 끊겨요

A: 여러 원인이 있어:
1. client.setKeepAlive(60)으로 킵얼라이브 시간 설정
2. client.loop()가 너무 오래 걸리는 코드 때문에 타임아웃 발생 → delay() 대신 millis() 기반 타이머 사용
3. Wi-Fi 신호 불안정 → WiFi.setAutoReconnect(true) 설정
4. MQTT 버퍼 크기 부족 → client.setBufferSize(512) 또는 더 크게 설정

❓ Q: DHT22가 NaN을 반환해요

A: DHT22는 최소 2초 간격으로 읽어야 해. 너무 빠르게 읽으면 NaN이 나와.
또한 데이터 핀에 4.7kΩ 풀업 저항을 VCC와 데이터 핀 사이에 연결해줘.
ESP32의 경우 내부 풀업을 사용할 수도 있어: pinMode(DHT_PIN, INPUT_PULLUP)

❓ Q: WS2812B LED가 이상한 색으로 켜져요

A: WS2812B는 GRB 순서야, RGB가 아니야! FastLED에서 COLOR_ORDER GRB로 설정했는지 확인해.
또한 데이터 라인에 330Ω 저항을 직렬로 연결하고, ESP32와 LED 스트립의 GND를 공통 연결했는지 확인해.

❓ Q: 릴레이가 ESP32 리셋 시 잠깐 켜졌다가 꺼져요

A: ESP32 부팅 시 GPIO 핀이 잠깐 HIGH가 되는 현상이야.
LOW 활성화 릴레이를 사용하고, setup()에서 pinMode() 전에 digitalWrite(RELAY_PIN, HIGH)를 먼저 실행해줘.
또는 릴레이 모듈의 IN 핀에 10kΩ 풀업 저항을 연결해서 부팅 시 HIGH 상태를 유지시켜.

❓ Q: Home Assistant에서 기기가 "사용 불가" 상태로 표시돼요

A: MQTT LWT(Last Will and Testament)를 설정하고, ESP32가 연결될 때 "online" 메시지를 발행하도록 해.
HA에서 availability_topic을 설정하면 기기 온라인/오프라인 상태를 정확히 표시할 수 있어.
또한 retain: true로 상태 메시지를 발행하면 HA 재시작 후에도 마지막 상태를 유지해.

❓ Q: 여러 ESP32를 사용할 때 MQTT 클라이언트 ID가 충돌해요

A: ESP32의 MAC 주소를 클라이언트 ID에 포함시켜:
String clientId = "esp32_" + WiFi.macAddress();
MAC 주소는 기기마다 고유하기 때문에 충돌이 없어.

▶유용한 개발 도구들

도구용도링크/설치
MQTT ExplorerMQTT 메시지 모니터링 GUImqtt-explorer.com
PlatformIOVS Code 기반 ESP32 개발환경platformio.org
Node-RED시각적 자동화 플로우 편집기nodered.org
ESPHomeYAML 기반 ESP32 펌웨어 생성esphome.io
TasmotaESP8266/32 오픈소스 펌웨어tasmota.github.io
Fritzing회로도 설계 도구fritzing.org
💡 ESPHome 추천! 코드 없이 YAML 파일만으로 ESP32 펌웨어를 만들 수 있어. Home Assistant와 완벽하게 통합되고, OTA 업데이트도 자동으로 지원해. 처음 시작하는 분들에게 특히 추천해!

🎉

마무리 — 나만의 스마트홈을 완성해봐!

자, 여기까지 왔으면 이제 스마트홈 IoT 제어의 핵심을 다 파악한 거야! 🎊
정리하자면:

📌 핵심 요약
✅
하드웨어: ESP32 (엣지 디바이스) + 라즈베리파이 (허브) 조합이 최고의 가성비
✅
조명 제어: PWM으로 밝기 조절, FastLED로 RGB 제어, MQTT로 원격 제어
✅
전원 제어: 릴레이 모듈로 220V AC 제어, Tasmota 펌웨어로 스마트 플러그 구현
✅
센서 연동: DHT22(온습도), PIR(움직임), BH1750(조도) 데이터를 MQTT로 발행
✅
통신: MQTT Pub/Sub 패턴, Mosquitto 브로커, LWT로 기기 상태 관리
✅
플랫폼: Home Assistant로 통합 관리, MQTT Discovery로 자동 기기 등록
✅
보안: MQTT 인증, IoT 네트워크 분리, OTA 업데이트로 안전하게 운영

처음에는 LED 하나 켜고 끄는 것부터 시작해서, 점점 센서를 추가하고, 자동화 규칙을 만들다 보면 어느새 완전한 스마트홈이 완성돼 있을 거야. 🏠✨

이런 IoT 프로젝트를 혼자 하기 어렵거나, 더 전문적인 도움이 필요하다면 재능넷에서 IoT/임베디드 개발 전문가를 찾아볼 수도 있어. 하드웨어 설계부터 소프트웨어 개발, Home Assistant 커스터마이징까지 다양한 전문가들이 도움을 줄 수 있거든!

자, 이제 납땜 인두 들고 시작해봐! 💪🔥

댓글 작성

이 글에 대한 여러분의 생각을 들려주세요

댓글 0
댓글을 불러오는 중입니다.

아직 댓글이 없습니다.