市販のスマートホーム製品は便利ですが、価格が高く、自分の使いたい機能がなかったり、メーカーのサービス終了リスクもあります。実は、格安マイコンを使えば市販品の10分の1の価格で、自分好みのスマートホームシステムを構築できるのです。
この記事では、電子工作初心者でも理解できるよう、格安マイコン「ESP32」を中心に、総額1万円以下で実用的なスマートホームシステムを作る方法を詳しく解説します。実際に動作するコード例と回路図も掲載しているので、記事を読みながらすぐに実践できます。
なぜ格安マイコンでスマートホームなのか?
市販品との比較
項目 | 市販スマートホーム | 自作スマートホーム |
---|---|---|
初期費用 | 5万円〜20万円 | 5千円〜1万円 |
カスタマイズ性 | 限定的 | 完全に自由 |
拡張性 | メーカー依存 | 無限大 |
学習効果 | なし | プログラミング・電子工作スキル向上 |
保守性 | メーカー次第 | 自分でメンテナンス可能 |
ESP32を選ぶ理由
ESP32は中国Espressif Systems社製のマイコンチップで、以下の特徴があります:
- Wi-Fi・Bluetooth内蔵:追加モジュール不要
- 低価格:開発ボード1000円程度
- 低消費電力:電池駆動も可能
- 豊富なI/O:34本のGPIOピン
- Arduino IDE対応:プログラミングが簡単
- デュアルコア:複数タスクの並行処理が可能
システム全体構成の設計
今回構築するスマートホームシステムの全体像を紹介します。
[センサーノード群] [中央制御ハブ] [制御デバイス群]
ESP32 + 温湿度センサー → ESP32メインハブ → ESP32 + リレー(照明)
ESP32 + 人感センサー → (MQTT Broker) → ESP32 + サーボ(カーテン)
ESP32 + ドアセンサー ↕ → ESP32 + モーター(換気扇)
スマホアプリ
(Webブラウザ)
システムの特徴
- 分散型アーキテクチャ:各デバイスが独立動作
- MQTT通信:軽量で信頼性の高い通信プロトコル
- Webベース操作:専用アプリ不要
- 拡張性:新しいデバイスを簡単に追加可能
必要な部品と総費用
基本システム(温湿度監視+照明制御)
部品名 | 数量 | 単価 | 小計 |
---|---|---|---|
ESP32開発ボード | 3個 | 1,200円 | 3,600円 |
DHT22温湿度センサー | 1個 | 800円 | 800円 |
リレーモジュール | 1個 | 300円 | 300円 |
ブレッドボード | 2個 | 200円 | 400円 |
ジャンパー線セット | 1セット | 500円 | 500円 |
抵抗・LED等小物 | – | 300円 | 300円 |
microUSBケーブル | 3本 | 200円 | 600円 |
合計 | – | – | 6,500円 |
拡張オプション
拡張機能 | 追加部品 | 追加費用 |
---|---|---|
人感センサー | PIRセンサー | 400円 |
ドア開閉センサー | リードスイッチ | 200円 |
カーテン自動開閉 | サーボモーター | 1,500円 |
土壌水分監視 | 土壌湿度センサー | 600円 |
カメラ監視 | ESP32-CAM | 1,800円 |
第1段階:温湿度監視システムの構築
最初に、基本となる温湿度監視システムを作ります。これだけでも十分実用的です。
回路構成
ESP32 (センサーノード) ESP32 (表示ハブ)
┌─────────────┐ ┌─────────────┐
│ D2 ●────────┼─DHT22 │ │
│ 3V3● │ │ OLED表示 │
│ GND●────────┼─GND │ (オプション) │
│ Wi-Fi内蔵 │ │ Web Server │
└─────────────┘ └─────────────┘
センサーノード用プログラム
#include <WiFi.h>
#include <PubSubClient.h>
#include <DHT.h>
#include <ArduinoJson.h>
// Wi-Fi設定
const char* ssid = "あなたのWiFi_SSID";
const char* password = "あなたのWiFiパスワード";
// MQTT設定
const char* mqtt_server = "192.168.1.100"; // ハブのIPアドレス
const char* device_id = "temp_sensor_01";
// センサー設定
#define DHT_PIN 2
#define DHT_TYPE DHT22
DHT dht(DHT_PIN, DHT_TYPE);
WiFiClient espClient;
PubSubClient client(espClient);
void setup() {
Serial.begin(115200);
dht.begin();
// Wi-Fi接続
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("WiFi connected!");
// MQTT接続設定
client.setServer(mqtt_server, 1883);
client.setCallback(callback);
}
void loop() {
if (!client.connected()) {
reconnect();
}
client.loop();
// 30秒ごとにセンサーデータを送信
static unsigned long lastSend = 0;
if (millis() - lastSend > 30000) {
sendSensorData();
lastSend = millis();
}
// 省電力のため軽くスリープ
delay(1000);
}
void sendSensorData() {
float temperature = dht.readTemperature();
float humidity = dht.readHumidity();
if (isnan(temperature) || isnan(humidity)) {
Serial.println("センサー読み取りエラー");
return;
}
// JSON形式でデータを送信
DynamicJsonDocument doc(200);
doc["device_id"] = device_id;
doc["temperature"] = temperature;
doc["humidity"] = humidity;
doc["timestamp"] = millis();
doc["battery"] = getBatteryLevel(); // 電池レベル(オプション)
String jsonString;
serializeJson(doc, jsonString);
client.publish("sensors/environment", jsonString.c_str());
Serial.printf("送信: 温度=%.1f℃, 湿度=%.1f%%\n", temperature, humidity);
}
float getBatteryLevel() {
// ADCで電源電圧を測定(簡易実装)
uint32_t voltage = analogRead(A0) * 3300 / 4096; // mV単位
return voltage / 1000.0; // V単位で返す
}
void callback(char* topic, byte* payload, unsigned int length) {
// 制御コマンドの受信処理(将来の拡張用)
Serial.printf("メッセージ受信 [%s]: ", topic);
for (int i = 0; i < length; i++) {
Serial.print((char)payload[i]);
}
Serial.println();
}
void reconnect() {
while (!client.connected()) {
Serial.print("MQTT接続試行中...");
if (client.connect(device_id)) {
Serial.println("MQTT接続成功!");
client.subscribe("commands/+"); // コマンド受信用
} else {
Serial.printf("MQTT接続失敗, rc=%d 5秒後に再試行\n", client.state());
delay(5000);
}
}
}
ハブ(サーバー)用プログラム
#include <WiFi.h>
#include <WebServer.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include <SPIFFS.h>
// Wi-Fi設定
const char* ssid = "あなたのWiFi_SSID";
const char* password = "あなたのWiFiパスワード";
WebServer server(80);
// センサーデータ保存用
struct SensorData {
float temperature;
float humidity;
unsigned long timestamp;
float battery;
String device_id;
};
const int MAX_READINGS = 100;
SensorData readings[MAX_READINGS];
int readingIndex = 0;
// MQTT Broker(簡易実装)
WiFiServer mqttBroker(1883);
void setup() {
Serial.begin(115200);
// SPIFFS初期化(ファイルシステム)
if (!SPIFFS.begin(true)) {
Serial.println("SPIFFS初期化失敗");
return;
}
// Wi-Fi接続
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.print("WiFi接続完了! IP: ");
Serial.println(WiFi.localIP());
// Webサーバー設定
setupWebServer();
server.begin();
// MQTT Broker開始
mqttBroker.begin();
Serial.println("MQTTブローカー開始");
}
void loop() {
server.handleClient();
handleMqttClients();
// システム監視
static unsigned long lastCheck = 0;
if (millis() - lastCheck > 60000) { // 1分ごと
systemHealthCheck();
lastCheck = millis();
}
}
void setupWebServer() {
// メインページ
server.on("/", HTTP_GET, []() {
String html = generateDashboardHTML();
server.send(200, "text/html", html);
});
// センサーデータAPI
server.on("/api/sensors", HTTP_GET, []() {
String json = generateSensorJSON();
server.send(200, "application/json", json);
});
// 制御API
server.on("/api/control", HTTP_POST, []() {
if (server.hasArg("device") && server.hasArg("command")) {
String device = server.arg("device");
String command = server.arg("command");
sendControlCommand(device, command);
server.send(200, "text/plain", "コマンド送信完了");
} else {
server.send(400, "text/plain", "パラメータ不足");
}
});
}
String generateDashboardHTML() {
String html = R"(
<!DOCTYPE html>
<html lang='ja'>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<title>スマートホーム ダッシュボード</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
margin: 0;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: rgba(255,255,255,0.95);
border-radius: 15px;
padding: 30px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
}
.header {
text-align: center;
margin-bottom: 40px;
color: #333;
}
.header h1 {
margin: 0;
font-size: 2.5rem;
font-weight: 300;
}
.sensor-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 25px;
margin-bottom: 40px;
}
.sensor-card {
background: white;
border-radius: 12px;
padding: 25px;
box-shadow: 0 8px 25px rgba(0,0,0,0.1);
transition: transform 0.2s ease;
}
.sensor-card:hover {
transform: translateY(-5px);
}
.sensor-title {
font-size: 1.1rem;
color: #666;
margin-bottom: 15px;
font-weight: 500;
}
.sensor-value {
font-size: 2.5rem;
font-weight: 300;
color: #2c3e50;
margin-bottom: 5px;
}
.sensor-unit {
font-size: 1rem;
color: #7f8c8d;
}
.controls {
background: white;
border-radius: 12px;
padding: 25px;
box-shadow: 0 8px 25px rgba(0,0,0,0.1);
}
.controls h3 {
margin-top: 0;
color: #2c3e50;
font-size: 1.3rem;
font-weight: 500;
}
button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 12px 25px;
margin: 8px;
border-radius: 25px;
cursor: pointer;
font-size: 1rem;
transition: all 0.2s ease;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
.status {
padding: 8px 16px;
border-radius: 20px;
font-size: 0.9rem;
font-weight: 500;
}
.status.online {
background: #d5f4e6;
color: #2d5a3d;
}
.status.offline {
background: #ffeaa7;
color: #d63031;
}
.last-update {
color: #7f8c8d;
font-size: 0.9rem;
margin-top: 10px;
}
</style>
</head>
<body>
<div class='container'>
<div class='header'>
<h1>🏠 スマートホーム ダッシュボード</h1>
</div>
<div class='sensor-grid'>
<div class='sensor-card'>
<div class='sensor-title'>リビング温度</div>
<div class='sensor-value' id='temperature'>--</div>
<div class='sensor-unit'>°C</div>
<div class='status online'>オンライン</div>
<div class='last-update' id='temp-update'>--</div>
</div>
<div class='sensor-card'>
<div class='sensor-title'>リビング湿度</div>
<div class='sensor-value' id='humidity'>--</div>
<div class='sensor-unit'>%</div>
<div class='status online'>オンライン</div>
<div class='last-update' id='humid-update'>--</div>
</div>
<div class='sensor-card'>
<div class='sensor-title'>システム稼働時間</div>
<div class='sensor-value' id='uptime'>--</div>
<div class='sensor-unit'>時間</div>
<div class='status online'>正常動作中</div>
</div>
</div>
<div class='controls'>
<h3>🎛️ デバイス制御</h3>
<button onclick='controlDevice("living_light", "toggle")'>💡 リビング照明</button>
<button onclick='controlDevice("bedroom_light", "toggle")'>🛏️ 寝室照明</button>
<button onclick='controlDevice("fan", "toggle")'>💨 換気扇</button>
<button onclick='controlDevice("curtain", "open")'>🪟 カーテン開</button>
<button onclick='controlDevice("curtain", "close")'>🌙 カーテン閉</button>
</div>
</div>
<script>
function updateData() {
fetch('/api/sensors')
.then(response => response.json())
.then(data => {
if (data.length > 0) {
const latest = data[data.length - 1];
document.getElementById('temperature').textContent = latest.temperature.toFixed(1);
document.getElementById('humidity').textContent = latest.humidity.toFixed(1);
const updateTime = new Date(latest.timestamp).toLocaleString('ja-JP');
document.getElementById('temp-update').textContent = '更新: ' + updateTime;
document.getElementById('humid-update').textContent = '更新: ' + updateTime;
}
const uptime = Math.floor(Date.now() / 1000 / 3600);
document.getElementById('uptime').textContent = uptime;
})
.catch(error => console.error('データ取得エラー:', error));
}
function controlDevice(device, command) {
const formData = new FormData();
formData.append('device', device);
formData.append('command', command);
fetch('/api/control', {
method: 'POST',
body: formData
})
.then(response => response.text())
.then(result => {
alert(result);
})
.catch(error => {
console.error('制御エラー:', error);
alert('制御に失敗しました');
});
}
// 5秒ごとにデータを更新
setInterval(updateData, 5000);
updateData(); // 初回実行
</script>
</body>
</html>
)";
return html;
}
String generateSensorJSON() {
DynamicJsonDocument doc(2048);
JsonArray array = doc.to<JsonArray>();
int count = min(10, readingIndex); // 最新10件
for (int i = 0; i < count; i++) {
JsonObject reading = array.createNestedObject();
reading["temperature"] = readings[i].temperature;
reading["humidity"] = readings[i].humidity;
reading["timestamp"] = readings[i].timestamp;
reading["battery"] = readings[i].battery;
reading["device_id"] = readings[i].device_id;
}
String jsonString;
serializeJson(doc, jsonString);
return jsonString;
}
void handleMqttClients() {
// 簡易MQTT処理(実際のプロダクトではライブラリ使用推奨)
WiFiClient client = mqttBroker.available();
if (client) {
Serial.println("MQTT クライアント接続");
while (client.connected()) {
if (client.available()) {
String message = client.readStringUntil('\n');
processMqttMessage(message);
}
delay(10);
}
client.stop();
}
}
void processMqttMessage(String message) {
DynamicJsonDocument doc(512);
deserializeJson(doc, message);
if (doc.containsKey("temperature")) {
// センサーデータの保存
readings[readingIndex % MAX_READINGS].temperature = doc["temperature"];
readings[readingIndex % MAX_READINGS].humidity = doc["humidity"];
readings[readingIndex % MAX_READINGS].timestamp = millis();
readings[readingIndex % MAX_READINGS].battery = doc["battery"];
readings[readingIndex % MAX_READINGS].device_id = doc["device_id"].as<String>();
readingIndex++;
Serial.printf("センサーデータ保存: %.1f℃, %.1f%%\n",
doc["temperature"].as<float>(), doc["humidity"].as<float>());
}
}
void sendControlCommand(String device, String command) {
// 制御コマンドをMQTTで配信
DynamicJsonDocument doc(200);
doc["device"] = device;
doc["command"] = command;
doc["timestamp"] = millis();
String jsonString;
serializeJson(doc, jsonString);
Serial.printf("制御コマンド送信: %s -> %s\n", device.c_str(), command.c_str());
// 実際の実装では、MQTT Publishで各デバイスに送信
}
void systemHealthCheck() {
Serial.println("=== システムヘルスチェック ===");
Serial.printf("稼働時間: %lu分\n", millis() / 60000);
Serial.printf("空きメモリ: %d bytes\n", ESP.getFreeHeap());
Serial.printf("Wi-Fi信号強度: %d dBm\n", WiFi.RSSI());
Serial.printf("登録センサー数: %d\n", min(readingIndex, MAX_READINGS));
}
第2段階:照明制御システムの追加
温湿度監視が動作したら、次は照明制御を追加します。
回路構成
ESP32 (照明制御ノード)
┌─────────────┐
│ D4 ●────────┼─リレー制御 → 照明
│ D2 ●────────┼─状態LED
│ 3V3● │
│ GND●────────┼─GND
│ Wi-Fi内蔵 │
└─────────────┘
照明制御ノード用プログラム
#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
// Wi-Fi・MQTT設定
const char* ssid = "あなたのWiFi_SSID";
const char* password = "あなたのWiFiパスワード";
const char* mqtt_server = "192.168.1.100";
const char* device_id = "living_light";
// ピン定義
#define RELAY_PIN 4
#define STATUS_LED_PIN 2
#define BUTTON_PIN 0 // 手動操作用ボタン
WiFiClient espClient;
PubSubClient client(espClient);
bool lightState = false;
unsigned long lastButtonPress = 0;
void setup() {
Serial.begin(115200);
// ピン設定
pinMode(RELAY_PIN, OUTPUT);
pinMode(STATUS_LED_PIN, OUTPUT);
pinMode(BUTTON_PIN, INPUT_PULLUP);
// 初期状態
digitalWrite(RELAY_PIN, LOW);
digitalWrite(STATUS_LED_PIN, LOW);
// Wi-Fi接続
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
// 接続中は LEDを点滅
digitalWrite(STATUS_LED_PIN, !digitalRead(STATUS_LED_PIN));
}
Serial.println("WiFi connected!");
digitalWrite(STATUS_LED_PIN, HIGH); // 接続完了
// MQTT設定
client.setServer(mqtt_server, 1883);
client.setCallback(onMqttMessage);
Serial.println("照明制御ノード開始");
}
void loop() {
if (!client.connected()) {
reconnect();
}
client.loop();
// 手動ボタンの処理
handleButton();
// 状態の定期送信
static unsigned long lastStatusSend = 0;
if (millis() - lastStatusSend > 60000) { // 1分ごと
sendStatus();
lastStatusSend = millis();
}
delay(50);
}
void handleButton() {
if (digitalRead(BUTTON_PIN) == LOW) {
if (millis() - lastButtonPress > 500) { // チャタリング防止
toggleLight();
lastButtonPress = millis();
Serial.println("手動操作: 照明切り替え");
}
}
}
void onMqttMessage(char* topic, byte* payload, unsigned int length) {
// JSONメッセージをパース
DynamicJsonDocument doc(256);
deserializeJson(doc, payload, length);
String device = doc["device"];
String command = doc["command"];
if (device == device_id) {
if (command == "on") {
setLight(true);
} else if (command == "off") {
setLight(false);
} else if (command == "toggle") {
toggleLight();
} else if (command == "status") {
sendStatus();
}
Serial.printf("コマンド実行: %s\n", command.c_str());
}
}
void setLight(bool state) {
lightState = state;
digitalWrite(RELAY_PIN, state ? HIGH : LOW);
digitalWrite(STATUS_LED_PIN, state ? HIGH : LOW);
Serial.printf("照明: %s\n", state ? "ON" : "OFF");
// 状態変更を通知
sendStatus();
}
void toggleLight() {
setLight(!lightState);
}
void sendStatus() {
DynamicJsonDocument doc(200);
doc["device_id"] = device_id;
doc["type"] = "light";
doc["state"] = lightState ? "on" : "off";
doc["timestamp"] = millis();
doc["ip_address"] = WiFi.localIP().toString();
doc["signal_strength"] = WiFi.RSSI();
String jsonString;
serializeJson(doc, jsonString);
client.publish("status/lights", jsonString.c_str());
}
void reconnect() {
while (!client.connected()) {
Serial.print("MQTT接続試行中...");
if (client.connect(device_id)) {
Serial.println("接続成功!");
// 制御コマンドを購読
client.subscribe("commands/lights");
client.subscribe("commands/all");
sendStatus(); // 接続完了通知
} else {
Serial.printf("接続失敗, rc=%d 5秒後に再試行\n", client.state());
delay(5000);
}
}
}
第3段階:高度な機能の実装
基本システムが動作したら、以下の高度な機能を追加できます。
スケジュール機能
// 時刻管理とスケジュール機能
#include <WiFi.h>
#include <time.h>
struct Schedule {
int hour;
int minute;
String device;
String action;
bool enabled;
};
Schedule schedules[] = {
{7, 0, "living_light", "on", true}, // 朝7時に照明ON
{23, 0, "living_light", "off", true}, // 夜11時に照明OFF
{6, 30, "curtain", "open", true}, // 朝6:30にカーテン開
{19, 0, "curtain", "close", true}, // 夕方7時にカーテン閉
};
void setupTime() {
configTime(9 * 3600, 0, "ntp.nict.jp"); // JST設定
Serial.print("NTP同期中");
struct tm timeinfo;
while (!getLocalTime(&timeinfo)) {
delay(500);
Serial.print(".");
}
Serial.println("\nNTP同期完了");
}
void checkSchedule() {
struct tm timeinfo;
if (!getLocalTime(&timeinfo)) {
return;
}
int currentHour = timeinfo.tm_hour;
int currentMinute = timeinfo.tm_min;
for (int i = 0; i < sizeof(schedules) / sizeof(schedules[0]); i++) {
if (schedules[i].enabled &&
schedules[i].hour == currentHour &&
schedules[i].minute == currentMinute) {
// スケジュール実行
executeScheduledAction(schedules[i]);
// 1分間は同じスケジュールを実行しない
schedules[i].enabled = false;
Serial.printf("スケジュール実行: %s -> %s (%02d:%02d)\n",
schedules[i].device.c_str(),
schedules[i].action.c_str(),
currentHour, currentMinute);
}
}
// 毎時0分にスケジュールを再有効化
if (currentMinute == 0) {
for (int i = 0; i < sizeof(schedules) / sizeof(schedules[0]); i++) {
schedules[i].enabled = true;
}
}
}
void executeScheduledAction(Schedule schedule) {
DynamicJsonDocument doc(200);
doc["device"] = schedule.device;
doc["command"] = schedule.action;
doc["source"] = "scheduler";
doc["timestamp"] = millis();
String jsonString;
serializeJson(doc, jsonString);
client.publish("commands/scheduled", jsonString.c_str());
}
自動化ルール(条件付き動作)
// 条件付き自動制御
struct AutomationRule {
String name;
String triggerDevice;
String triggerCondition;
float triggerValue;
String actionDevice;
String actionCommand;
bool enabled;
unsigned long lastTriggered;
};
AutomationRule rules[] = {
{"温度制御", "temp_sensor_01", "temperature_above", 28.0, "fan", "on", true, 0},
{"湿度制御", "temp_sensor_01", "humidity_above", 70.0, "dehumidifier", "on", true, 0},
{"人感照明", "motion_sensor_01", "motion_detected", 1.0, "living_light", "on", true, 0},
{"省エネ", "temp_sensor_01", "temperature_below", 20.0, "heater", "off", true, 0},
};
void processAutomationRules(String deviceId, float value, String parameter) {
for (int i = 0; i < sizeof(rules) / sizeof(rules[0]); i++) {
if (!rules[i].enabled || rules[i].triggerDevice != deviceId) {
continue;
}
bool shouldTrigger = false;
unsigned long now = millis();
// クールダウン時間(5分)
if (now - rules[i].lastTriggered < 300000) {
continue;
}
// 条件判定
if (rules[i].triggerCondition == parameter + "_above") {
shouldTrigger = (value > rules[i].triggerValue);
} else if (rules[i].triggerCondition == parameter + "_below") {
shouldTrigger = (value < rules[i].triggerValue);
} else if (rules[i].triggerCondition == parameter + "_equal") {
shouldTrigger = (abs(value - rules[i].triggerValue) < 0.1);
}
if (shouldTrigger) {
executeAutomationAction(rules[i]);
rules[i].lastTriggered = now;
Serial.printf("自動化ルール実行: %s (%.1f %s %.1f)\n",
rules[i].name.c_str(), value,
rules[i].triggerCondition.c_str(), rules[i].triggerValue);
}
}
}
void executeAutomationAction(AutomationRule rule) {
DynamicJsonDocument doc(200);
doc["device"] = rule.actionDevice;
doc["command"] = rule.actionCommand;
doc["source"] = "automation";
doc["rule"] = rule.name;
doc["timestamp"] = millis();
String jsonString;
serializeJson(doc, jsonString);
client.publish("commands/automation", jsonString.c_str());
}
第4段階:実践的な追加デバイス
カーテン自動開閉システム
// カーテン制御用ESP32プログラム
#include <ESP32Servo.h>
#define SERVO_PIN 18
#define POSITION_SENSOR_PIN 34 // カーテン位置検知用
Servo curtainServo;
int curtainPosition = 0; // 0=閉, 100=開
void setup() {
curtainServo.attach(SERVO_PIN);
pinMode(POSITION_SENSOR_PIN, INPUT);
// 初期位置の検出
detectInitialPosition();
}
void controlCurtain(String command) {
if (command == "open") {
moveCurtainToPosition(100);
} else if (command == "close") {
moveCurtainToPosition(0);
} else if (command.startsWith("position_")) {
int targetPos = command.substring(9).toInt();
targetPos = constrain(targetPos, 0, 100);
moveCurtainToPosition(targetPos);
}
}
void moveCurtainToPosition(int targetPosition) {
int currentPos = curtainPosition;
int step = (targetPosition > currentPos) ? 1 : -1;
Serial.printf("カーテン移動: %d%% → %d%%\n", currentPos, targetPosition);
while (currentPos != targetPosition) {
currentPos += step;
// サーボ角度計算(0-100% → 0-180度)
int servoAngle = map(currentPos, 0, 100, 0, 180);
curtainServo.write(servoAngle);
delay(50); // スムーズな動作のための遅延
// 位置フィードバック
if (currentPos % 10 == 0) {
sendPositionUpdate(currentPos);
}
}
curtainPosition = targetPosition;
Serial.printf("カーテン移動完了: %d%%\n", curtainPosition);
sendPositionUpdate(curtainPosition);
}
void detectInitialPosition() {
// 位置センサーやエンドストップを使用して現在位置を検出
int sensorValue = analogRead(POSITION_SENSOR_PIN);
curtainPosition = map(sensorValue, 0, 4095, 0, 100);
Serial.printf("初期カーテン位置: %d%%\n", curtainPosition);
}
void sendPositionUpdate(int position) {
DynamicJsonDocument doc(200);
doc["device_id"] = "curtain_controller";
doc["type"] = "curtain";
doc["position"] = position;
doc["state"] = (position > 50) ? "open" : "closed";
doc["timestamp"] = millis();
String jsonString;
serializeJson(doc, jsonString);
client.publish("status/curtains", jsonString.c_str());
}
植物自動水やりシステム
// 植物管理システム
#define MOISTURE_SENSOR_PIN 32
#define WATER_PUMP_PIN 25
#define WATER_LEVEL_SENSOR_PIN 33
struct PlantStatus {
int moisture;
bool needsWater;
int waterLevel;
unsigned long lastWatered;
};
PlantStatus plant;
void setup() {
pinMode(WATER_PUMP_PIN, OUTPUT);
digitalWrite(WATER_PUMP_PIN, LOW);
// 初期状態
plant.lastWatered = 0;
}
void checkPlantStatus() {
// 土壌水分測定
int moistureRaw = analogRead(MOISTURE_SENSOR_PIN);
plant.moisture = map(moistureRaw, 0, 4095, 0, 100);
// 水タンク残量測定
int waterLevelRaw = analogRead(WATER_LEVEL_SENSOR_PIN);
plant.waterLevel = map(waterLevelRaw, 0, 4095, 0, 100);
// 水やり判定
plant.needsWater = (plant.moisture < 30); // 30%以下で水やり
Serial.printf("植物状態 - 水分: %d%%, 水タンク: %d%%, 要水やり: %s\n",
plant.moisture, plant.waterLevel,
plant.needsWater ? "YES" : "NO");
sendPlantStatus();
}
void waterPlant() {
if (plant.waterLevel < 10) {
Serial.println("エラー: 水タンクが空です");
sendAlert("水タンクの水を補充してください");
return;
}
if (millis() - plant.lastWatered < 3600000) { // 1時間以内の重複防止
Serial.println("警告: 最近水やりしたばかりです");
return;
}
Serial.println("自動水やり開始");
// 水やり実行(3秒間)
digitalWrite(WATER_PUMP_PIN, HIGH);
delay(3000);
digitalWrite(WATER_PUMP_PIN, LOW);
plant.lastWatered = millis();
Serial.println("自動水やり完了");
// 30秒後に水分再測定
delay(30000);
checkPlantStatus();
}
void sendPlantStatus() {
DynamicJsonDocument doc(300);
doc["device_id"] = "plant_monitor";
doc["moisture"] = plant.moisture;
doc["water_level"] = plant.waterLevel;
doc["needs_water"] = plant.needsWater;
doc["last_watered"] = plant.lastWatered;
doc["timestamp"] = millis();
String jsonString;
serializeJson(doc, jsonString);
client.publish("sensors/plants", jsonString.c_str());
}
void sendAlert(String message) {
DynamicJsonDocument doc(200);
doc["type"] = "alert";
doc["device_id"] = "plant_monitor";
doc["message"] = message;
doc["severity"] = "warning";
doc["timestamp"] = millis();
String jsonString;
serializeJson(doc, jsonString);
client.publish("alerts/system", jsonString.c_str());
}
セキュリティとプライバシー対策
スマートホームシステムではセキュリティが重要です。
基本的なセキュリティ実装
// WiFi・MQTT認証強化
#include <WiFiClientSecure.h>
// MQTT認証情報
const char* mqtt_username = "your_mqtt_user";
const char* mqtt_password = "your_strong_password";
// デバイス認証用のユニークID
String getDeviceUID() {
uint64_t chipid = ESP.getEfuseMac();
return String((uint32_t)(chipid >> 32), HEX) + String((uint32_t)chipid, HEX);
}
// 暗号化通信(簡易実装)
String encryptMessage(String message, String key) {
// 実際のプロダクトではAES等の強力な暗号化を使用
String encrypted = "";
for (int i = 0; i < message.length(); i++) {
encrypted += char(message[i] ^ key[i % key.length()]);
}
return encrypted;
}
// セキュアなMQTT接続
bool connectSecureMQTT() {
if (client.connect(device_id.c_str(), mqtt_username, mqtt_password)) {
Serial.println("セキュアMQTT接続成功");
// デバイス認証
DynamicJsonDocument authDoc(200);
authDoc["device_id"] = device_id;
authDoc["uid"] = getDeviceUID();
authDoc["timestamp"] = millis();
String authMessage;
serializeJson(authDoc, authMessage);
client.publish("auth/devices", authMessage.c_str());
return true;
}
return false;
}
// 異常アクセス検知
unsigned long lastCommandTime = 0;
int commandCount = 0;
bool isValidCommand(String command, String source) {
unsigned long now = millis();
// レート制限(1分間に10回まで)
if (now - lastCommandTime < 60000) {
commandCount++;
if (commandCount > 10) {
Serial.println("警告: コマンド制限を超過");
sendSecurityAlert("過剰なコマンド実行を検知");
return false;
}
} else {
commandCount = 1;
lastCommandTime = now;
}
// 許可されたコマンドのチェック
String allowedCommands[] = {"on", "off", "toggle", "status", "open", "close"};
bool isAllowed = false;
for (String allowed : allowedCommands) {
if (command == allowed) {
isAllowed = true;
break;
}
}
if (!isAllowed) {
Serial.printf("警告: 不正なコマンド: %s\n", command.c_str());
sendSecurityAlert("不正なコマンドを検知: " + command);
return false;
}
return true;
}
void sendSecurityAlert(String message) {
DynamicJsonDocument doc(250);
doc["type"] = "security_alert";
doc["message"] = message;
doc["device_id"] = device_id;
doc["ip_address"] = WiFi.localIP().toString();
doc["timestamp"] = millis();
doc["severity"] = "high";
String jsonString;
serializeJson(doc, jsonString);
client.publish("security/alerts", jsonString.c_str());
}
トラブルシューティングとメンテナンス
システム監視とログ機能
// システム監視
void systemMonitoring() {
static unsigned long lastCheck = 0;
if (millis() - lastCheck < 300000) return; // 5分ごと
SystemStatus status;
// メモリ使用量
status.freeHeap = ESP.getFreeHeap();
status.heapSize = ESP.getHeapSize();
status.memoryUsage = 100.0 * (status.heapSize - status.freeHeap) / status.heapSize;
// Wi-Fi状態
status.wifiRSSI = WiFi.RSSI();
status.wifiConnected = (WiFi.status() == WL_CONNECTED);
// MQTT状態
status.mqttConnected = client.connected();
// 稼働時間
status.uptime = millis() / 1000;
// CPU温度(ESP32内蔵センサー)
status.cpuTemperature = temperatureRead();
// 異常検知
checkSystemHealth(status);
// ステータス送信
sendSystemStatus(status);
lastCheck = millis();
}
void checkSystemHealth(SystemStatus status) {
// メモリ不足警告
if (status.memoryUsage > 85.0) {
sendAlert("メモリ使用率が高すぎます: " + String(status.memoryUsage) + "%");
}
// Wi-Fi信号強度警告
if (status.wifiRSSI < -70) {
sendAlert("Wi-Fi信号が弱いです: " + String(status.wifiRSSI) + "dBm");
}
// CPU温度警告
if (status.cpuTemperature > 70.0) {
sendAlert("CPU温度が高すぎます: " + String(status.cpuTemperature) + "°C");
}
// 接続状態確認
if (!status.wifiConnected) {
Serial.println("Wi-Fi再接続試行");
WiFi.reconnect();
}
if (!status.mqttConnected) {
Serial.println("MQTT再接続試行");
reconnect();
}
}
// 自動復旧機能
void autoRecovery() {
static int recoveryAttempts = 0;
static unsigned long lastRecovery = 0;
if (millis() - lastRecovery < 300000) return; // 5分間隔
// Wi-Fi接続チェック
if (WiFi.status() != WL_CONNECTED) {
Serial.println("Wi-Fi自動復旧試行");
WiFi.disconnect();
delay(1000);
WiFi.begin(ssid, password);
recoveryAttempts++;
}
// MQTT接続チェック
if (WiFi.status() == WL_CONNECTED && !client.connected()) {
Serial.println("MQTT自動復旧試行");
reconnect();
recoveryAttempts++;
}
// 復旧試行回数が多い場合は再起動
if (recoveryAttempts > 5) {
Serial.println("自動復旧失敗 - システム再起動");
sendAlert("システム自動再起動");
delay(1000);
ESP.restart();
}
// 正常動作時は復旧試行回数をリセット
if (WiFi.status() == WL_CONNECTED && client.connected()) {
recoveryAttempts = 0;
}
lastRecovery = millis();
}
拡張可能性とアップデート
OTA(Over The Air)アップデート機能
#include <ArduinoOTA.h>
void setupOTA() {
ArduinoOTA.setHostname(device_id.c_str());
ArduinoOTA.setPassword("your_ota_password");
ArduinoOTA.onStart([]() {
String type = (ArduinoOTA.getCommand() == U_FLASH) ? "sketch" : "filesystem";
Serial.println("OTAアップデート開始: " + type);
// アップデート中は他の処理を停止
client.publish("status/update", "アップデート開始");
});
ArduinoOTA.onEnd([]() {
Serial.println("\nOTAアップデート完了");
client.publish("status/update", "アップデート完了");
});
ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
});
ArduinoOTA.onError([](ota_error_t error) {
Serial.printf("OTAエラー[%u]: ", error);
String errorMsg = "";
if (error == OTA_AUTH_ERROR) errorMsg = "認証失敗";
else if (error == OTA_BEGIN_ERROR) errorMsg = "開始エラー";
else if (error == OTA_CONNECT_ERROR) errorMsg = "接続エラー";
else if (error == OTA_RECEIVE_ERROR) errorMsg = "受信エラー";
else if (error == OTA_END_ERROR) errorMsg = "終了エラー";
Serial.println(errorMsg);
client.publish("status/update", ("アップデートエラー: " + errorMsg).c_str());
});
ArduinoOTA.begin();
Serial.println("OTA準備完了");
}
void handleOTA() {
ArduinoOTA.handle();
}
運用コストと電力管理
省電力運用
// 省電力設定
void setupPowerManagement() {
// CPU周波数を下げる(240MHz → 80MHz)
setCpuFrequencyMhz(80);
// Wi-Fi省電力モード
WiFi.setSleep(true);
// 不要なペリフェラルを停止
btStop(); // Bluetooth停止
Serial.println("省電力モード設定完了");
}
// Deep Sleep実装(バッテリー駆動デバイス用)
void enterDeepSleep(unsigned long sleepTimeSeconds) {
Serial.printf("Deep Sleep開始 - %lu秒\n", sleepTimeSeconds);
// 状態を保存
saveDeviceState();
// スリープ設定
esp_sleep_enable_timer_wakeup(sleepTimeSeconds * 1000000); // マイクロ秒単位
// GPIO wakeup設定(ボタンでの起動)
esp_sleep_enable_ext0_wakeup(GPIO_NUM_0, 0);
// Deep Sleep実行
esp_deep_sleep_start();
}
void saveDeviceState() {
// EEPROM等に現在の状態を保存
preferences.begin("device_state", false);
preferences.putBool("light_state", lightState);
preferences.putInt("curtain_pos", curtainPosition);
preferences.end();
Serial.println("デバイス状態保存完了");
}
void loadDeviceState() {
preferences.begin("device_state", true);
lightState = preferences.getBool("light_state", false);
curtainPosition = preferences.getInt("curtain_pos", 0);
preferences.end();
// 復元した状態を適用
setLight(lightState);
moveCurtainToPosition(curtainPosition);
Serial.println("デバイス状態復元完了");
}
まとめ:次のステップとさらなる発展
プロジェクトの成果
この記事で紹介したシステムを実装すると、以下の機能を持つスマートホームが完成します:
基本機能
- 温湿度の遠隔監視
- 照明の遠隔・自動制御
- Webブラウザからの操作
- スマートフォン対応
発展機能
- スケジュール自動制御
- 条件付き自動化ルール
- カーテン自動開閉
- 植物自動水やり
- セキュリティ監視
- システム自動復旧
総コスト振り返り
システム規模 | 費用 | 機能レベル |
---|---|---|
基本システム | 6,500円 | 市販品3-5万円相当 |
中規模システム | 12,000円 | 市販品8-12万円相当 |
大規模システム | 20,000円 | 市販品20-30万円相当 |
さらなる発展の方向性
1. AI・機械学習の導入
- 使用パターンの学習
- 異常検知の高度化
- 予測的メンテナンス
2. 音声制御の追加
- Google Assistant連携
- Amazon Alexa連携
- 自作音声認識
3. 外部サービス連携
- 天気予報API連携
- 電気料金最適化
- SNS通知機能
4. 商用レベルの機能
- プロフェッショナルな管理画面
- データベース永続化
- ユーザー管理機能
学習効果とスキルアップ
このプロジェクトを通じて以下のスキルが身につきます:
- 電子工作:回路設計、センサー活用
- プログラミング:C++、JavaScript、HTML/CSS
- ネットワーク:Wi-Fi、MQTT、HTTP通信
- システム設計:分散システム、IoTアーキテクチャ
- 運用・保守:監視、ログ解析、トラブルシューティング
最後に
格安マイコンを使ったスマートホームは、単なる電子工作を超えて、現代のIoTシステム開発の本質を学べる素晴らしい教材です。市販品では実現できない、完全にカスタマイズされた自分だけのシステムを構築する楽しさを、ぜひ体験してください。
小さく始めて、徐々に機能を拡張していくことで、必ず実用的なシステムに成長します。この記事が、あなたのスマートホーム構築の第一歩となることを願っています。
始めるなら今すぐ! まずはESP32とDHT22センサーを購入して、温度監視から始めてみましょう。きっと想像以上に簡単で、そして楽しいはずです。