알렉사 연동 아두이노 홈 컨트롤러: 음성으로 스마트홈 제어하기
부품 및 소모품
| 부품 | 수량 |
| Arduino Yun | 1 |
| ControlEverything.com 4채널 I2C 릴레이 컨트롤러 | 1 |
| DHT11 온습도 센서 (4핀) | 1 |
| 10kΩ 저항 | 2 |
| SparkFun RedBot 부저 | 1 |
| 범용 NPN 트랜지스터 | 1 |
| LM2596 DC-DC 벅 컨버터 | 1 |
| Amazon Alexa Echo Dot | 1 |
| 아두이노 프로토 실드 | 1 |
| USB 웹캠 | 1 |
| 흰색 정션 박스 (200×155×80mm) | 1 |
필요한 도구 및 장비
앱 및 온라인 서비스
- Arduino IDE
- Amazon Alexa Skills Kit
- Amazon Web Services AWS Lambda
- PubNub Publish/Subscribe API
프로젝트 개요
개요
아마존 알렉사와 에코 닷 출시로 자연어 음성 명령으로 제어하는 스마트홈의 가능성이 열렸습니다. 이 프로젝트는 다양한 센서와 기기를 인터넷에 연결하고, 아마존 알렉사 스킬로 제어하는 아두이노 홈 컨트롤러를 만듭니다.
제 스킬 "Arduino Home Controller"를 설치해 바로 쓰거나, 이 튜토리얼을 따라 직접 스킬을 만들 수 있습니다.
제어 대상
- 거실, 주방, 차고, 침실 조명 4개 (릴레이 제어)
- 온습도 센서 (DHT11)
- 부저 알람
- 웹캠으로 보안 사진 촬영 후 이메일 전송
데모 영상에서는 브레드보드 위 LED 4개로 각 방 조명을 시뮬레이션했습니다. 원격 모듈로 실제 조명을 제어하려면 제 다른 프로젝트 "Arduino Home Controller Remote Modules"를 참조하세요.
블록 다이어그램
4대 핵심 구성요소:
- Amazon Echo Dot: 음성 명령 수신 → AWS 전송
- AWS (Alexa Skill + Lambda Function): 명령 해석. 스킬명 "Arduino Home Controller"
- MQTT 브로커 (PubNub 클라우드): 제어 메시지 중계. 설정/읽기용 2개 채널 사용. 데모용으로 제 PubNub 계정 사용 가능.
- 홈 컨트롤러 (Arduino Yun): 이더넷 내장 + 리눅스 머신 탑재. 파이썬으로 일부 동작 처리. 릴레이 제어, 센서 읽기, 부저, 웹캠 제어.
소프트웨어 설치 및 설정
옵션 1: 데모용 - 내 스킬 사용
알렉사 앱에서 "Arduino Home Controller" 스킬 검색 → 활성화 → 아마존 계정 연동 (이메일은 ID용, 메일 발송 안 함).
옵션 2: 직접 스킬 만들기
1. 스킬 생성 (Amazon Developer Console)
- Custom Interaction Model, English US
- 스킬명: TestSkill, 호출명: "Test Skill"
- Intent Schema, Custom Slot Types(States, HabRooms), Sample Utterances 정의 (본문 참조)
2. Lambda 함수 생성 (AWS Console)
- Author from scratch, Node.js 6.10
- Custom Role (IAM: Lambda_Basic_Execution, IoT 권한 추가)
- Alexa Skills Kit 트리거 추가, Skill ID 연결
- 코드 업로드 (data.zip → index.js), Application ID 수정
3. 스킬 설정 마무리
- Endpoint: AWS Lambda ARN 연결
- Account Linking: Security Profile 생성 (Login with Amazon), Redirect URL, Client ID/Secret 설정
- Global Fields: 카테고리 "Smart Home", 설명, 아이콘 등
- Privacy & Compliance 완료
4. 테스트 스킬 설치
알렉사 앱 → Your Skills → Dev Skills → TestSkill 활성화 → 계정 연동 → "알렉사, 테스트 스킬에게 물어봐"로 테스트.
아두이노 Yun 설정
처음 사용하는 경우 Greg Baugues 가이드 참조해 초기 설정 필요.
추가 소프트웨어 설치 (SSH 접속 필요):
ssh root@yourYunName.local # 기본 비밀번호: arduino
opkg update
opkg install kmod-video-uvc # UVC 드라이버
opkg install python-openssl # 이메일용
opkg install fswebcam # 사진 촬영 유틸
SD 카드 마운트 확인(mount → /dev/sda1 on /mnt/sda1 type vfat), 카메라 테스트(fswebcam test.png).
이메일 발송 파이썬 스크립트 (AHC_SendEmail.py) SD 카드에 복사 후 Gmail 계정/비밀번호, 서버 정보 수정. 테스트: python /mnt/sda1/AHC_SendEmail.py /mnt/sda1/ test.png
하드웨어 연결: 회로도 참조해 DHT11, 부저 등 연결.
스케치 업로드 (ArduinoHomeMQTT.ino):
필요 라이브러리(ArduinoJson, DHT_sensor_library, pubsubclient-master)를 libraries.zip으로 제공. 아두이노 IDE에서 보드 "Arduino Yun" 선택, 코드 내 Amazon 이메일(16번 줄)과 UUID(50번 줄) 수정 후 업로드. 시리얼 모니터로 PubNub 연결 및 센서 초기화 확인.
알렉사 발화 예시 테스트
- 조명: "알렉사, 아두이노 홈에게 거실 조명 켜줘/꺼줘" (방/주방/차고/거실)
- 알람: "알렉사, 아두이노 홈에게 알람 켜줘/꺼줘"
- 온도/습도: "알렉사, 아두이노 홈에게 온도/습도 읽어줘"
- 사진: "알렉사, 아두이노 홈에게 사진 찍어줘"
- 도움말: "알렉사, 아두이노 홈에게 대해/도움말"
소프트웨어 상세 구조
GitHub에서 전체 파일 다운로드 가능.
ArduinoHomeMQTT: 아두이노 윤 스케치 + 라이브러리
lambda: AWS Lambda용 Node.js 함수 (+ node_modules)
python: 사진 이메일 전송 파이썬 스크립트
schematic: 프리칭(Fritzing) 회로도 파일
워크플로우:
(1) Echo Dot → 음성 명령 → Skill
(2) Skill → Lambda 함수 (index.js)
(3) Lambda → 의도 해석 → MQTT JSON 메시지 → PubNub 브로커
(4) 메시지 형식: {"topic": "Topic_xxxx", "command": "on/off", "id": "이메일주소"}
(5) 채널 AHC_IOT_01로 발행, AHC_IOT_02로 응답 수신
(6) 아두이노 윤 스케치 → 브로커 구독/발행
(7) Bridge로 리눅스와 통신 (WAN/LAN, 명령 실행)
(8) 이메일 전송 시 파이썬 스크립트, 사진 촬영 시 fswebcam 호출
하드웨어 상세
핵심: Arduino Yun. 디지털 핀 할당:
Pin 2: DHT11 센서
Pin 8: 부저 알람 (NPN 트랜지스터로 구동)
Pin 9: 침실 조명
Pin 10: 주방 조명
Pin 11: 차고 조명
Pin 12: 거실 조명
웹캠: USB 포트 연결. 12V DC 전원 → 벅 컨버터로 5V 변환해 전체 공급. 프로토 실드에 커넥터, 인터페이스 회로, 전원 회로 구성. 확장용 여분 공간 확보. 플라스틱 박스에 아두이노 윤, 실드, 릴레이, 레귤레이터 수납, 알람/웹캠/센서는 외부에 노출.
데모용으로는 브레드보드 LED 4개로 조명 시뮬레이션.
이 가이드를 읽어주셔서 감사합니다. 제안 사항은 연락 주세요. 작년 돌아가신 아버지를 추모하며 이 프로젝트를 만듭니다. R.I.P Dad.
코드
ArduinoHomeMQTT.ino (아두이노)
// ArduinoHomeMQTT v2.1
// Copyright (C)2018 Jose Cruz. All rights reserved.
// web: https://sites.google.com/view/raeiot/home
#include <YunClient.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include <DHT.h>
#include <Process.h>
#define MyID "YOUR AMAZON EMAIL LINKED IN ALEXA APP"
#define DHTPIN 2
#define DHTTYPE DHT11
DHT dht(DHTPIN, DHTTYPE);
#define alarm 8
#define room_light 9
#define kitchen_light 10
#define garage_light 11
#define livingroom_light 12
int mqtt_server_port = 1883;
const char* mqtt_server = "mqtt.pndsn.com";
const char* topic = "AHC_IOT_01";
const char* topic2 = "AHC_IOT_02";
const char* clientId = "pub-c-e93def7f-95aa-475a-aa60-cc3cd32ee8a7/sub-c-ec04dbbc-0893-11e8-8e75-cea83f8405bb/YOUR UUID GENERATED";
YunClient ethClient;
PubSubClient client(ethClient);
Process picture;
String filename;
String path = "/mnt/sda1/";
void TakePhoto() {
filename = "";
picture.runShellCommand("date +%s");
while (picture.running());
while (picture.available() > 0) { char c = picture.read(); filename += c; }
filename.trim(); filename += ".jpg";
picture.runShellCommand("/usr/bin/fswebcam -i 0 --jpeg 95 --no-banner --fps 1 -S 1 -r 352x288 --save " + path + filename);
while (picture.running());
picture.runShellCommand("python /mnt/sda1/AHC_SendEmail.py " + path + " " + filename);
while (picture.running());
}
void SendTemperature() {
char cstr[16];
float t = dht.readTemperature();
client.publish(topic2, itoa(t, cstr, 10));
}
void SendHumidity() {
char cstr[16];
float h = dht.readHumidity();
client.publish(topic2, itoa(h, cstr, 10));
}
void callback(char* topic, byte* payload, unsigned int length) {
Serial.print("Message arrived ["); Serial.print(topic); Serial.print("] ");
for (int i = 0; i < length; i++) Serial.print((char)payload[i]);
Serial.println();
StaticJsonBuffer<200> jsonBuffer;
JsonObject& root = jsonBuffer.parseObject((char*)payload);
String alexa_topic = root["topic"];
String alexa_command = root["command"];
String alexa_id = root["id"];
if (alexa_id.equals(MyID)) {
if (alexa_topic.endsWith("Topic_room")) digitalWrite(room_light, alexa_command.endsWith("on") ? LOW : HIGH);
else if (alexa_topic.endsWith("Topic_kitchen")) digitalWrite(kitchen_light, alexa_command.endsWith("on") ? LOW : HIGH);
else if (alexa_topic.endsWith("Topic_garage")) digitalWrite(garage_light, alexa_command.endsWith("on") ? LOW : HIGH);
else if (alexa_topic.endsWith("Topic_living room")) digitalWrite(livingroom_light, alexa_command.endsWith("on") ? LOW : HIGH);
else if (alexa_topic.endsWith("Topic_temperature")) SendTemperature();
else if (alexa_topic.endsWith("Topic_humidity")) SendHumidity();
else if (alexa_topic.endsWith("Topic_alarm")) digitalWrite(alarm, alexa_command.endsWith("on") ? HIGH : LOW);
else if (alexa_topic.endsWith("Topic_photo")) TakePhoto();
}
}
void reconnect() {
while (!client.connected()) {
Serial.print("Attempting MQTT connection...");
if (client.connect(clientId)) {
Serial.println("MQTT broker connected");
client.subscribe(topic);
} else {
Serial.print("Failed, rc = "); Serial.print(client.state());
Serial.println(" try again in 5 seconds");
delay(5000);
}
}
}
void setup() {
Bridge.begin(); delay(2000);
Serial.begin(115200);
Serial.println("Init Arduino Home Controller v2.1...");
pinMode(alarm, OUTPUT);
pinMode(room_light, OUTPUT);
pinMode(kitchen_light, OUTPUT);
pinMode(garage_light, OUTPUT);
pinMode(livingroom_light, OUTPUT);
digitalWrite(room_light, HIGH);
digitalWrite(kitchen_light, HIGH);
digitalWrite(garage_light, HIGH);
digitalWrite(livingroom_light, HIGH);
client.setServer(mqtt_server, mqtt_server_port);
client.setCallback(callback);
client.connect(clientId);
Serial.println("Connected to PubNub MQTT broker OK ...");
client.subscribe(topic);
Serial.println("Suscribe to topic at MQTT broker Ok ...");
dht.begin(); delay(4000);
Serial.println("DHT sensor Ok ...");
}
void loop() {
if (!client.connected()) reconnect();
client.loop();
}
AHC_SendEmail.py (파이썬)
import smtplib, sys, time
from email.MIMEMultipart import MIMEMultipart
from email.MIMEText import MIMEText
from email.MIMEBase import MIMEBase
from email import encoders
fromaddr = "joseacruzp@gmail.com"
toaddr = "joseacruzp@gmail.com"
emailpassword = "YOUR PASSWORD"
subject = "Arduino Home Controller by Alexa Skill"
body = "Photo taken in " + time.strftime('%a, %d %b %Y %H:%M:%S %Z(%z)')
filedir = sys.argv[1]
filename = sys.argv[2]
msg = MIMEMultipart()
msg['From'] = fromaddr
msg['To'] = toaddr
msg['Subject'] = subject
msg.attach(MIMEText(body, 'plain'))
attachment = open(filedir + filename, "rb")
part = MIMEBase('application', 'octet-stream')
part.set_payload(attachment.read())
encoders.encode_base64(part)
part.add_header('Content-Disposition', "attachment; filename= %s" % filename)
msg.attach(part)
server = smtplib.SMTP('smtp.gmail.com', 587)
server.starttls()
server.login(fromaddr, emailpassword)
server.sendmail(fromaddr, toaddr, msg.as_string())
server.quit()
index.js (AWS Lambda, Node.js)
// Alexa SDK for Arduino Home Controller
// Copyright (c) 2014-2015 Amazon.com, Inc. or its affiliates.
var PubNub = require("pubnub");
var pubnub = new PubNub({
ssl: true,
publish_key: "pub-c-e93def7f-95aa-475a-aa60-cc3cd32ee8a7",
subscribe_key: "sub-c-ec04dbbc-0893-11e8-8e75-cea83f8405bb",
uuid: "YOUR UUID"
});
var channel = 'AHC_IOT_01';
var channel2 = 'AHC_IOT_02';
var slotStates = ['on', 'off'];
var APP_ID = 'YOUR APP ID FOR THE SKILL';
var AlexaSkill = require('./AlexaSkill');
var ArduinoHomeSkill = function() { AlexaSkill.call(this, APP_ID); };
var mqttListener, access_token, url;
var https = require("https");
function SendMessage(topicname, commandvalue, speechOutput, iresponse) {
https.get(url, res => {
res.setEncoding("utf8");
let body = "";
res.on("data", data => { body += data; });
res.on("end", () => {
pubnub.publish({
channel: channel,
message: { "topic": topicname, "command": commandvalue, "id": JSON.parse(body).email }
}).then(response => {
console.log("message Published w/ timetoken", response.timetoken);
if (speechOutput) iresponse.tell(speechOutput);
}).catch(error => {
console.log("publishing failed w/ status: ", error);
iresponse.ask("Sorry, I didn't catch what you said");
});
});
});
}
ArduinoHomeSkill.prototype = Object.create(AlexaSkill.prototype);
ArduinoHomeSkill.prototype.constructor = ArduinoHomeSkill;
ArduinoHomeSkill.prototype.eventHandlers.onSessionStarted = function(sessionStartedRequest, session) {
console.log("ArduinoHomeSkill onSessionStarted requestId: " + sessionStartedRequest.requestId + ", sessionId: " + session.sessionId);
pubnub.removeListener(mqttListener);
pubnub.unsubscribeAll();
};
ArduinoHomeSkill.prototype.eventHandlers.onLaunch = function(launchRequest, session, response) {
console.log("ArduinoHomeSkill onLaunch requestId: " + launchRequest.requestId + ", sessionId: " + session.sessionId);
if (session.user.accessToken == undefined) {
response.tellWithCardLink('To start using this skill, please use the companion app to authenticate on Amazon.');
return;
}
response.ask("Welcome to Arduino Home Controller. What would you like to do?", "I am ready");
};
ArduinoHomeSkill.prototype.eventHandlers.onSessionEnded = function(sessionEndedRequest, session) {
console.log("ArduinoHomeSkill onSessionEnded requestId: " + sessionEndedRequest.requestId + ", sessionId: " + session.sessionId);
};
ArduinoHomeSkill.prototype.intentHandlers = {
"AboutIntent": function(intent, session, response) {
response.ask("Arduino Home Controller skill lets you control internet connected devices... What would you like to do?");
},
"LightIntent": function(intent, session, response) {
var slotHabRooms = ['room', 'kitchen', 'garage', 'living room'];
var lightSlotValue = intent.slots.light ? intent.slots.light.value : "";
var whichSlotValue = intent.slots.which ? intent.slots.which.value : "";
if (lightSlotValue && whichSlotValue && slotStates.indexOf(lightSlotValue.toLowerCase()) > -1 && slotHabRooms.indexOf(whichSlotValue.toLowerCase()) > -1) {
SendMessage('Topic_' + whichSlotValue, lightSlotValue, "The light is now " + lightSlotValue, response);
} else { response.ask("Sorry, I didn't catch what you said"); }
},
"AlarmIntent": function(intent, session, response) {
var alarmSlotValue = intent.slots.alarm ? intent.slots.alarm.value : "";
if (alarmSlotValue && slotStates.indexOf(alarmSlotValue.toLowerCase()) > -1) {
SendMessage('Topic_alarm', alarmSlotValue, "The alarm is now " + alarmSlotValue, response);
} else { response.ask("Sorry, I didn't catch what you said"); }
},
"TakePhotoIntent": function(intent, session, response) {
SendMessage('Topic_photo', 'take', "Taken home photo", response);
},
"GetTemperatureIntent": function(intent, session, response) {
mqttListener = {
status: function(statusEvent) {},
message: function(message) {
response.tell("Inside Temperature is " + message.message + " degrees C");
},
presence: function(presenceEvent) {}
};
pubnub.addListener(mqttListener);
pubnub.subscribe({ channels: [channel2] });
SendMessage('Topic_temperature', 'Ok', null, response);
},
"GetHumidityIntent": function(intent, session, response) {
mqttListener = {
status: function(statusEvent) {},
message: function(message) {
response.tell("Inside Humidity is " + message.message + " %");
},
presence: function(presenceEvent) {}
};
pubnub.addListener(mqttListener);
pubnub.subscribe({ channels: [channel2] });
SendMessage('Topic_humidity', 'Ok', null, response);
},
"AMAZON.HelpIntent": function(intent, session, response) {
response.ask("With Arduino Home Controller skill and Alexa you can control internet connected devices... What would you like to do?");
},
"AMAZON.StopIntent": function(intent, session, response) {
response.tell("Thanks for using Arduino Home Controller. Bye see you later");
},
"AMAZON.CancelIntent": function(intent, session, response) {
response.tell("Thanks for using Arduino Home Controller. Bye see you later");
},
default: function(intent, session, response) { response.ask("Try again"); }
};
exports.handler = function(event, context) {
try {
access_token = event['context']['System']['user']['accessToken'];
url = 'https://api.amazon.com/user/profile?access_token=' + access_token;
} catch (error) { console.log(error); }
var ArduinoHomeControl = new ArduinoHomeSkill();
ArduinoHomeControl.execute(event, context);
};
GitHub 저장소
https://github.com/jcruzp/ArduinoHomeController
회로도
