智能家居开关:手机 + Home Assistant 远程控制的继电器开关
- 做出一个能在手机 / Home Assistant 里远程开关的智能继电器设备
- 把 L2 的继电器控制和 L3 的智能家居接入两条线接成一件成品
- 学会用带光耦隔离的继电器模块,把弱电控制和强电负载安全隔开
- 用 esp-mqtt 走 MQTT discovery,让设备在 Home Assistant 里自动出现、被自动化调度
- 想清楚哪些活自己能干、哪些强电接线必须交给持证电工——这是本项目最重要的一课
| 器材 | 数量 | 参考 |
|---|---|---|
| ESP32-S3 开发板 | 1 | —约 25-45 元(以商城实际为准) |
| 继电器模块(必须带光耦隔离,1 路即可) | 1 | —约 4-12 元(以商城实际为准) |
| 接线端子 / 冷压端子(配合外壳固定强电线用) | 1 套 | —约 3-10 元(以商城实际为准) |
| 阻燃外壳(能把强电部分封住、留好走线孔) | 1 | —约 10-30 元(以商城实际为准) |
| 杜邦线(弱电侧接线用) | 若干 | —约 5-10 元(以商城实际为准) |
价格随渠道波动,以购买页实时为准。
想象你人在公司,突然想起阳台的补光灯忘了关。掏出手机点一下,"咔哒"一声,那盏灯应声灭掉——这就是这个项目要带你做出来的东西:一个能被手机、被 Home Assistant 远程控制的智能开关。
它是你第一次让自己焊的板子去指挥现实世界里一个真实的、可能带强电的负载,而不再只是点一颗 LED。这一步跨得比前几个项目都大,因为它第一次把两条你分别学过的线接到了一起:L2 的继电器控制让你懂了"弱电怎么去掰一个大开关",L3 的智能家居接入让你懂了"设备怎么进 Home Assistant 被远程调度"。这个项目就是把这两半拼成一件成品。
但正因为它可能碰到强电,动手前我必须把话说死——这是它和前两个台灯、传感器项目最本质的区别:
220V 市电会致命,这不是修辞。 这个项目的控制部分(ESP32 + 继电器线圈侧)是安全的弱电,你可以放心焊、放心调;但继电器触点那一侧一旦接上 220V 市电负载,就进入了要命的领域。
- 继电器必须选带光耦隔离的模块:光耦把单片机这侧和继电器驱动那侧用一束光隔开,强电的浪涌、漏电窜不回你的 ESP32。原理见 继电器与光耦,选型时认准模块上写着 "光耦隔离 / optocoupler isolated"。
- 强电侧的接线、走线间距、绝缘、入盒固定,属于持证电工的专业范畴。本文只讲到"继电器触点是干什么用的"这个原理层,不教任何带电接线操作。非专业人员不要自行改造市电线路、不要带电作业。
- 要真正接市电负载,请由持证电工完成,或全程彻底断电、断开总闸后操作,并把强电部分完整封进阻燃外壳,人手绝不可能碰到。
- 一切电压、电流、触点额定、隔离耐压参数以你手上器件的 datasheet 和当地电工规范为准,触点额定不得超标使用。
这一段不是免责套话。这个项目最值得你学会的,恰恰是"哪些活我自己能干、哪些必须交给电工"这条边界——一个成熟的硬件人,知道自己的能力圈在哪里。
下面绝大部分工作都在安全的弱电侧,你能完整跑通"手机控制一个继电器咔哒响"的全流程;真要驱动市电负载的最后一步,我会明确交回给持证电工。
第一步:想清楚要做成什么样,再定选型
动手前把成品行为钉死,选型才有依据。我们的智能开关就三个行为:本地咔哒(ESP32 一发指令,继电器触点吸合/断开、带动那一侧的负载)、手机 / HA 远程控制(通过 MQTT,手机上点一下继电器就动)、自动发现(一上电联网就出现在 Home Assistant 里、能被自动化调度,不用手动配)。
继电器模块:一定要带光耦隔离
这是本项目唯一不能省、不能凑合的选型红线。继电器模块分两种:一种光板直驱(引脚直接连继电器的驱动三极管),一种带光耦隔离(引脚信号先经过一颗光耦再去驱动继电器)。
必须选后者。 原因在 继电器与光耦 里讲透了:触点那侧一旦接强电,强电的浪涌、漏电有可能顺着共用的电气连接窜回单片机,轻则死机、重则烧板伤人。光耦用"光"把两侧彻底隔开,信号能过去、危险过不来。买的时候认准模块上那颗光耦芯片(常见 817 系列)、商品描述里的"光耦隔离"字样。多花那几块钱,买的是安全余量。
再说一个模块细节:继电器模块有 高电平触发 / 低电平触发 之分。低电平触发即"IN 脚给低电平时继电器吸合"。这不影响接线,但会影响代码里 GPIO 该置高还是置低,写码时我会标出来,你按自己模块调一个字。
ESP32-S3 板、外壳与接线端子
ESP32-S3 沿用你手头的板子,它负责联网、跑 MQTT、用一根 GPIO 去控继电器 IN 脚,这一侧全是 3.3V 弱电、安全。外壳必须是阻燃材质且能把强电部分完全封住——装好后人手碰不到带电金属;接线端子把强电线牢靠固定、避免松动打火。这两样属于强电装配环节,交给电工时他会处理,你负责选对材料。
第二步:接线——弱电侧动手,强电侧只讲原理
这个项目的接线天然分成两半,界线非常清楚:弱电侧你自己接,强电侧只理解原理、交给电工。
弱电侧(你来接,安全)
ESP32-S3 和继电器模块之间就三根线:
ESP32-S3 继电器模块(带光耦隔离)
3V3 ───────────▶ VCC
GND ───────────▶ GND
GPIO10 ─────────▶ IN (控制信号:置高/低 → 继电器吸合/断开)
选控制脚前,先把 S3 的选脚雷区过一遍:
ESP32-S3 选脚雷区,接 IN 脚前对照排除:
- GPIO0 / 3 / 45 / 46:strapping 脚,上电电平决定启动模式,接了外设可能让板子刷不进、起不来;
- GPIO26-37:绝大多数模组内部连着 SPI flash / PSRAM,动了直接死机;
- GPIO19 / 20:默认是 USB D-/D+,占用会断掉 USB 串口,连日志都看不到;
- GPIO22 / 23 / 24 / 25:ESP32-S3 上根本不存在这几个号(GPIO 号从 21 直接跳到 26),写了编译不报错、运行诡异。
避开这些,我选 GPIO10 当继电器控制脚。VCC 接 3V3(若你的模块标称 5V 供电,按模块说明供 5V,但光耦输入侧的 IN 信号仍按 3.3V 逻辑判断即可,以模块 datasheet 为准)。
弱电侧接完了。这一侧你可以完全放心地反复接、反复调——它跟市电没有任何电气连接,最坏就是继电器咔哒响。
强电侧(只讲原理,不教操作,交给电工)
继电器触点侧有三个端子:COM(公共端)、NO(常开)、NC(常闭)。它的作用就是替代墙上那个手动开关——把负载的一根火线"断在中间",让触点决定这根线通还是断。负载串在 COM 和 NO 之间,继电器不动作时 NO 断开、灯灭;ESP32 一发指令,线圈得电、触点吸到 NO、回路接通、灯亮。这就是"弱电指挥强电"的落点。
但具体怎么把火线接进 COM、怎么走线、怎么固定入盒、零地火怎么分——这些都属于持证电工的活,本文到此为止,不给任何带电接线步骤。 你要做的是把弱电部分调通、把外壳和端子备好,再请电工按当地电工规范完成强电侧接线与入盒。这条边界本身就是这个项目要教你的东西。
第三步:分步把代码写出来
代码分三步长出来,每步都能单独烧进去看到效果——出问题你才知道是哪一步坏的。这三步全程只在弱电侧,你听着继电器咔哒响就知道对了,完全不需要接任何强电。
步 1:先让 GPIO 把继电器控起来
第一步只验证一件事:GPIO 接对了、继电器模块认这个信号、能咔哒吸合。这段就是最朴素的 GPIO 输出:
#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
static const char *TAG = "switch";
#define RELAY_GPIO GPIO_NUM_10 // 继电器 IN 脚,避开雷区
// 你的模块若是"高电平触发":ON=1, OFF=0(下面按此写)
// 若是"低电平触发":把 ON/OFF 的值对调即可(这就是前面说的改一个字)
#define RELAY_ON 1
#define RELAY_OFF 0
static void relay_init(void)
{
gpio_config_t cfg = {
.pin_bit_mask = 1ULL << RELAY_GPIO,
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
gpio_config(&cfg);
gpio_set_level(RELAY_GPIO, RELAY_OFF); // 上电默认断开,别一通电就吸合
}
static void relay_set(bool on)
{
gpio_set_level(RELAY_GPIO, on ? RELAY_ON : RELAY_OFF);
ESP_LOGI(TAG, "继电器 -> %s", on ? "ON(吸合)" : "OFF(断开)");
}
void app_main(void)
{
relay_init();
while (1) { // 每 2 秒开关一次,测通路用
relay_set(true);
vTaskDelay(pdMS_TO_TICKS(2000));
relay_set(false);
vTaskDelay(pdMS_TO_TICKS(2000));
}
}
烧进去 idf.py build flash monitor。你应该看到:继电器每 2 秒"咔哒"吸合、再断开,模块指示灯跟着亮灭,串口同步打日志。听到这个稳定的节奏,说明 GPIO → 光耦 → 继电器这条控制通路全对——这一步把"接线 + 触发电平方向"这两个最容易错的地方先隔离验证掉。
如果继电器一直吸着不动、或者节奏反了,八成是你的模块是低电平触发,而代码按高电平写的。把
RELAY_ON/RELAY_OFF的值对调,重烧即可。前面说的"改一个字"就是这里。
步 2:接上 Wi-Fi 和 MQTT,让远程指令能进来
继电器能受控了,现在让它被网络上的指令控制。用 Home Assistant 那节提过的 MQTT 路线:ESP32 用 esp-mqtt 连上一个 Broker(HA 自带的 Mosquitto 插件最省事),订阅一个"命令 topic",收到 ON/OFF 就控继电器。
Wi-Fi 连接沿用你 L3 学过的 wifi_init_sta 事件组范式,这里聚焦 MQTT——初始化、订阅命令 topic、在回调里解析指令:
#include "mqtt_client.h"
#include <string.h>
#define MQTT_URI "mqtt://192.168.1.10:1883" // 换成你的 Broker 地址
#define TOPIC_CMD "home/switch1/set" // HA 往这里发 ON/OFF
#define TOPIC_STATE "home/switch1/state" // 我们把当前状态回报到这里
static esp_mqtt_client_handle_t s_client;
// 把当前继电器状态发回 state topic,让 HA 显示同步(retain 保证 HA 重启也能读到)
static void report_state(bool on)
{
esp_mqtt_client_publish(s_client, TOPIC_STATE,
on ? "ON" : "OFF", 0, 1, /*retain=*/1);
}
static void mqtt_event_handler(void *args, esp_event_base_t base,
int32_t event_id, void *event_data)
{
esp_mqtt_event_handle_t e = event_data;
switch ((esp_mqtt_event_id_t)event_id) {
case MQTT_EVENT_CONNECTED:
ESP_LOGI(TAG, "MQTT 已连上 Broker");
esp_mqtt_client_subscribe(s_client, TOPIC_CMD, 1); // 订阅命令
report_state(false); // 上线先报一次状态
break;
case MQTT_EVENT_DATA:
// 收到命令:比对 payload 是不是 "ON"
if (e->topic_len && strncmp(e->topic, TOPIC_CMD, e->topic_len) == 0) {
bool on = (e->data_len == 2 && strncmp(e->data, "ON", 2) == 0);
relay_set(on);
report_state(on); // 动作后立刻回报,闭环
}
break;
default:
break;
}
}
static void mqtt_start(void)
{
esp_mqtt_client_config_t cfg = {
.broker.address.uri = MQTT_URI,
// 若 Broker 设了账号密码,在这里补 .credentials.username / .authentication.password
};
s_client = esp_mqtt_client_init(&cfg);
esp_mqtt_client_register_event(s_client, ESP_EVENT_ANY_ID,
mqtt_event_handler, NULL);
esp_mqtt_client_start(s_client);
}
app_main 里的顺序改成:先 relay_init(),再连 Wi-Fi(你的 wifi_init_sta),Wi-Fi 拿到 IP 后再 mqtt_start()。
你应该看到:设备上线后,你在电脑上用 MQTT 客户端(比如 MQTTX)连同一个 Broker,往 home/switch1/set 发一条 ON,继电器咔哒吸合;发 OFF,断开。同时 home/switch1/state 同步回报当前状态。这就是"网络指令 → 继电器动作 → 状态回报"的完整闭环——注意我们每次动作后都回报了 state,这让 HA 里的开关显示和设备真实状态永远一致,是做成产品的关键细节。
步 3:走 MQTT discovery,让 HA 自动发现这个开关
现在设备能被 MQTT 控了,但你还得手动在 HA 里配一个 switch 才看得见它。MQTT discovery 省掉这步:设备一上线,主动往 HA 监听的 homeassistant/... 前缀发一条配置 JSON,声明"我是一个开关、命令收在哪、状态发在哪",HA 收到就自动创建一个开关实体。
在 MQTT_EVENT_CONNECTED 里、订阅命令之前,先发一条 discovery 配置:
// MQTT discovery:告诉 HA 这里有个开关,自动创建实体
static void publish_discovery(void)
{
// HA 约定的 discovery topic:homeassistant/<组件>/<对象id>/config
const char *disc_topic = "homeassistant/switch/switch1/config";
// 配置 JSON:把命令/状态 topic、名字、唯一 id 告诉 HA
// 字段含义以 HA MQTT switch 文档为准(见本页 sources)
const char *payload =
"{"
"\"name\":\"阳台开关\","
"\"unique_id\":\"esp32s3_switch1\","
"\"command_topic\":\"home/switch1/set\","
"\"state_topic\":\"home/switch1/state\","
"\"payload_on\":\"ON\","
"\"payload_off\":\"OFF\""
"}";
// retain=1:HA 重启后仍能从 Broker 读到这条配置,设备不必在线也认得它
esp_mqtt_client_publish(s_client, disc_topic, payload, 0, 1, /*retain=*/1);
ESP_LOGI(TAG, "已发送 HA discovery 配置");
}
在 MQTT_EVENT_CONNECTED 分支里,report_state 之前加一行 publish_discovery(); 即可。
你应该看到:设备一上线,打开 HA 主界面,通常会出现一个名叫"阳台开关"的实体(在"设置 → 设备与服务 → 实体"里也能找到)。点它,继电器咔哒响;进"设置 → 自动化",你甚至能写"日落时自动打开阳台开关"这类规则。到这,你的设备就和米家插座平起平坐地待在 HA 里、能被整套家庭逻辑调度了——这正是 L3 智能家居那节 讲的终点。
核心功能全齐了。回头看,你没写多少全新的东西:GPIO 控继电器是 L2 继电器 的底子,MQTT 接入是 L3 智能家居 的路线,你干的是把它们用 discovery 串成一个能自动进 HA 的成品——这个"串",就是 project 比 guide 多出来的那层功夫。
第四步:调试——对不上就查这张表
分步烧的好处是,哪一步出问题你已经缩小了范围。真出岔子,照这张表查:
| 现象 | 最可能的原因 | 怎么办 |
|---|---|---|
| 继电器完全不动、没声音 | IN 脚接错 / GPIO 号写错 / 模块没供电 | 确认 RELAY_GPIO 是你接 IN 的那根;用万用表量模块 VCC 有没有电;先单独跑步 1 |
| 继电器一直吸着、或开关节奏反了 | 模块是低电平触发,代码按高电平写 | 对调 RELAY_ON / RELAY_OFF 的值,重烧 |
| 上电就吸合、不受控 | relay_init 里没先置 OFF,或触发方向反了 |
确认 init 末尾有 gpio_set_level(RELAY_GPIO, RELAY_OFF);再核对触发电平方向 |
| 板子刷不进 / 一直重启 | IN 脚踩了 strapping(0/3/45/46)或 flash 区(26-37) | 换到 GPIO10 这类安全脚,对照第二步雷区表 |
| MQTT 连不上 Broker | Broker 地址/端口/账号密码错,或设备没连上网 | 先用电脑上 MQTTX 拿同样参数连一遍,确认 Broker 本身没问题,再回头查 MQTT_URI 和 Wi-Fi |
| 发 ON/OFF 继电器不动 | 命令 topic 拼错,或 payload 大小写对不上 | 确认发的 topic 和 TOPIC_CMD 完全一致;payload 严格是大写 ON/OFF |
| HA 里发现不了这个开关 | discovery topic 前缀不对,或 HA 没装 MQTT 集成 | 确认 HA 装了 MQTT 集成、discovery 前缀是默认 homeassistant;用 MQTTX 订阅 homeassistant/# 看配置有没有真的发出去 |
| HA 里能控但状态显示不同步 | 动作后没回报 state,或 state topic 对不上 | 确认每次 relay_set 后都调了 report_state;核对 state_topic 与代码里 TOPIC_STATE 一致 |
一次只改一处再烧。MQTT 这类联网调试尤其如此——同时改 topic、改电平方向、改 Broker 地址,出了错你根本分不清是哪个的锅。先用 MQTTX 这样的桌面客户端把 Broker 和 topic 单独验通,再回头调设备代码,能省你一大半时间。
第五步:从"能跑的 demo"做成"像样的产品"
到这它已经是个能远程控继电器的设备了。但"能控继电器咔哒响"和"能安全地当家里开关用"之间还差几步——有些你能自己做,有些必须交出去。
加物理按键(你能做)
纯靠手机控有个尴尬:网断了、手机没电了就没法开关了。加一个物理按键做本地兜底:一根 GPIO 接按键(去抖沿用 L2 那套 配套的输入处理),按一下翻转继电器状态,同时 report_state 一次让 HA 同步。本地、远程两条路都能开关,才像个正经开关。
状态回报做扎实(你能做)
我们已在每次动作后回报了 state,还能更稳:给 discovery 配置加 availability_topic,设备用 MQTT 遗嘱(LWT)在掉线时自动发一条"offline",HA 里这个开关就显示成"不可用"而非停在最后状态——避免你以为灯开着、其实设备早掉线了。
做成真正能挂墙上的产品(这一步必须找持证电工)
裸板加继电器模块只能算原型。想变成能接进家里线路、天天用的开关,绕不开强电装配:把强电部分完整封进阻燃外壳、火线经接线端子牢靠固定接入 COM、走线间距和绝缘达标、整体不漏电不打火。
这一步我必须明确交回给你:涉及市电的接线与入盒,请由持证电工完成,或在彻底断开总闸、全程无电的前提下操作,并确保封装后人手绝无可能触及带电金属。 这不是项目"没做完",而是它本来就该在这里划出边界——知道哪一步该收手、该交给专业的人,本身就是这个 R2 项目最该让你带走的东西。 想更系统地理解强电隔离为什么这么严,回 继电器与光耦 再读一遍那张安全框。
小结 · 你做出了什么、下一步去哪
- 你做出了一个能被手机、被 Home Assistant 远程控制、还能自动出现在 HA 里的智能继电器开关,从选型(认准光耦隔离)、弱电侧接线、分步写 ESP-IDF 码(GPIO 控继电器 → esp-mqtt 收指令 → MQTT discovery 自动发现)到调试,走完了弱电侧全流程。
- 你第一次把 L2 继电器控制 和 L3 智能家居接入 两条线,用 MQTT discovery + 状态回报接成一件成品——这个"组织零件"的功夫,就是 project 比 guide 多出来的那一层。
- 但这个项目真正想让你带走的,是那条能力边界:弱电侧你完全能独立搞定;强电侧的接线与入盒属于持证电工范畴,光耦隔离不可省、市电不可带电作业(原理见 继电器与光耦)。一个成熟的硬件人,靠的不只是会做多少,还有清楚地知道哪些不该自己做。
下一步:把物理按键和 availability 遗嘱补上让开关更完整;想玩更复杂的联动,那是 HA 自动化的天地,回 L3 智能家居 深挖。回看全部实战项目见项目总览。这个项目是你从"点灯"迈向"控真实负载"的分水岭——迈过去了,你才算真正碰到了硬件和现实世界打交道的那道门槛。
本文为公开资料整理,非亲测。关键参数与代码请结合实物与下列官方来源验证。