Node-Red 控制一週時間ON,OFF + WOKWI ESP32
- Node-RED Dashboard UI:可設定每週(週一~週日)的時間區間(開始/結束 時:分:秒)
- 會自動顯示目前時間
- 自動比對目前時間是否在設定區間內 → 輸出
1(ON) 或0(OFF) - 儲存/讀取設定到 file 節點 (JSON 檔)
- 顯示在
ui-table - 同步發佈狀態到 MQTT topic(例:
alex9ufo/schedule/status)
| 功能 | 節點 | 說明 |
|---|---|---|
| 設定每週時間 | ui_form | 可選星期與開始/結束時間 |
| 儲存設定 | file | 存成 /data/schedule.json |
| 顯示表格 | ui_table | 顯示所有設定 |
| 比對目前時間 | 每秒執行,判斷當前是否在設定區間 | |
| 狀態顯示 | ui_text | 顯示目前時間與 ON/OFF |
| MQTT 發佈 | topic: alex9ufo/schedule/status |
WOKWI程式
#include <WiFi.h>
#include <PubSubClient.h>
//const char* ssid = "你的WiFi名稱";
//const char* password = "你的WiFi密碼";
const char* ssid = "Wokwi-GUEST";
const char* password = "";
const char* mqtt_server = "broker.mqttgo.io";
WiFiClient espClient;
PubSubClient client(espClient);
const int LED_PIN = 2;
void callback(char* topic, byte* message, unsigned int length) {
String msg;
for (int i = 0; i < length; i++) msg += (char)message[i];
Serial.print("收到主題: "); Serial.println(topic);
Serial.print("訊息: "); Serial.println(msg);
if (msg == "1") {
digitalWrite(LED_PIN, HIGH);
client.publish("alex9ufo/schedule/led", "ON");
} else {
digitalWrite(LED_PIN, LOW);
client.publish("alex9ufo/schedule/led", "OFF");
}
}
void reconnect() {
while (!client.connected()) {
if (client.connect("ESP32_TimerClient")) {
client.subscribe("alex9ufo/schedule/status");
} else {
delay(2000);
}
}
}
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) delay(500);
client.setServer(mqtt_server, 1883);
client.setCallback(callback);
}
void loop() {
if (!client.connected()) reconnect();
client.loop();
}
Node-Red程式
[
{
"id": "d84e42b6dbd0994e",
"type": "ui_form",
"z": "c7fde6141e82d752",
"name": "時間設定表單",
"label": "每週時間設定",
"group": "e5af8077ea7bc427",
"order": 8,
"width": 11,
"height": 1,
"options": [
{
"label": "星期",
"value": "weekday",
"type": "text",
"required": false,
"rows": null
},
{
"label": "開始時間 (HH:MM:SS)",
"value": "start",
"type": "text",
"required": false,
"rows": null
},
{
"label": "結束時間 (HH:MM:SS)",
"value": "end",
"type": "text",
"required": false,
"rows": null
}
],
"formValue": {
"weekday": "",
"start": "",
"end": ""
},
"payload": "",
"submit": "儲存設定",
"cancel": "",
"topic": "schedule/add",
"topicType": "str",
"splitLayout": false,
"className": "",
"x": 120,
"y": 120,
"wires": [
[
"e4ab434ed5e4c543"
]
]
},
{
"id": "e4ab434ed5e4c543",
"type": "function",
"z": "c7fde6141e82d752",
"name": "新增時間設定 + 儲存檔案",
"func": "let list = flow.get('scheduleList') || [];\nlet d = msg.payload;\nlet found = false;\nfor (let i=0;i<list.length;i++) {\n if (list[i].weekday === d.weekday) {\n list[i] = d;\n found = true;\n break;\n }\n}\nif (!found) list.push(d);\nflow.set('scheduleList', list);\nreturn [{payload: JSON.stringify(list, null, 2)}, {payload: list}];",
"outputs": 2,
"x": 350,
"y": 120,
"wires": [
[
"8dad8d745a795a75"
],
[
"692e2f012e2919fe"
]
]
},
{
"id": "8dad8d745a795a75",
"type": "file",
"z": "c7fde6141e82d752",
"name": "寫入 schedule.json",
"filename": "/data/schedule.json",
"appendNewline": false,
"createDir": true,
"overwriteFile": "true",
"encoding": "none",
"x": 590,
"y": 100,
"wires": [
[]
]
},
{
"id": "692e2f012e2919fe",
"type": "ui_table",
"z": "c7fde6141e82d752",
"group": "3b8f4b37e0b6b986",
"name": "顯示清單",
"order": 1,
"width": 8,
"height": 6,
"columns": [
{
"field": "weekday",
"title": "星期",
"width": "20%",
"align": "left",
"formatter": "plaintext",
"formatterParams": {
"target": "_blank"
}
},
{
"field": "start",
"title": "開始時間",
"width": "40%",
"align": "left",
"formatter": "plaintext",
"formatterParams": {
"target": "_blank"
}
},
{
"field": "end",
"title": "結束時間",
"width": "40%",
"align": "left",
"formatter": "plaintext",
"formatterParams": {
"target": "_blank"
}
}
],
"outputs": 0,
"cts": false,
"x": 800,
"y": 120,
"wires": []
},
{
"id": "9635e178025ac66d",
"type": "inject",
"z": "c7fde6141e82d752",
"name": "每秒更新時間",
"props": [
{
"p": "payload"
}
],
"repeat": "1",
"once": true,
"topic": "",
"payloadType": "date",
"x": 140,
"y": 260,
"wires": [
[
"4c7f615aae1de37f"
]
]
},
{
"id": "4c7f615aae1de37f",
"type": "function",
"z": "c7fde6141e82d752",
"name": "比對時間輸出 on/off",
"func": "let now = new Date();\nlet wd = now.getDay();\nlet dayMap = ['日','一','二','三','四','五','六'];\nlet weekday = dayMap[wd];\nlet nowTime = now.toTimeString().split(' ')[0];\nlet list = flow.get('scheduleList') || [];\nlet manual = flow.get('manualOverride') || 'auto';\nlet status = 0;\n\nif (manual === 'forceOn') status = 1;\nelse if (manual === 'forceOff') status = 0;\nelse {\n for (let s of list) {\n if (s.weekday === weekday && nowTime >= s.start && nowTime <= s.end) {\n status = 1;\n break;\n }\n }\n}\nmsg.payload = {\n now: now.toLocaleString(),\n weekday: weekday,\n status: status,\n mode: manual\n};\nflow.set('currentStatus', status);\nreturn [msg, {payload: status}];",
"outputs": 2,
"x": 330,
"y": 260,
"wires": [
[
"f5470011762e1185"
],
[
"3ae28406032feb5a"
]
]
},
{
"id": "f5470011762e1185",
"type": "ui_text",
"z": "c7fde6141e82d752",
"group": "e5af8077ea7bc427",
"order": 1,
"width": 11,
"height": 1,
"name": "目前時間顯示",
"label": "目前時間",
"format": "{{msg.payload.now}} ({{msg.payload.weekday}}) 狀態:{{msg.payload.status == 1 ? '🟢ON' : '⚪OFF'}} 模式:{{msg.payload.mode}}",
"layout": "row-spread",
"className": "",
"style": false,
"font": "",
"fontSize": "",
"color": "#000000",
"x": 540,
"y": 240,
"wires": []
},
{
"id": "3ae28406032feb5a",
"type": "mqtt out",
"z": "c7fde6141e82d752",
"name": "發佈 MQTT 狀態",
"topic": "alex9ufo/schedule/status",
"qos": "0",
"retain": "true",
"respTopic": "",
"contentType": "",
"userProps": "",
"correl": "",
"expiry": "",
"broker": "192c2b20bef1e71a",
"x": 540,
"y": 280,
"wires": []
},
{
"id": "df251f90a866b0da",
"type": "mqtt in",
"z": "c7fde6141e82d752",
"name": "接收 LED 狀態",
"topic": "alex9ufo/schedule/led",
"qos": "2",
"datatype": "auto-detect",
"broker": "192c2b20bef1e71a",
"nl": false,
"rap": false,
"inputs": 0,
"x": 120,
"y": 400,
"wires": [
[
"1f5d4d36e2781dd9"
]
]
},
{
"id": "1f5d4d36e2781dd9",
"type": "ui_text",
"z": "c7fde6141e82d752",
"group": "e5af8077ea7bc427",
"order": 2,
"width": 11,
"height": 1,
"name": "LED 回傳狀態",
"label": "ESP32 LED 狀態",
"format": "LED 狀態:{{msg.payload}}",
"layout": "row-spread",
"className": "",
"style": false,
"font": "",
"fontSize": "",
"color": "#000000",
"x": 320,
"y": 400,
"wires": []
},
{
"id": "ed18247f43c38979",
"type": "ui_button",
"z": "c7fde6141e82d752",
"name": "手動開啟",
"group": "e5af8077ea7bc427",
"order": 3,
"width": 3,
"height": 1,
"passthru": false,
"label": "手動 ON",
"tooltip": "",
"color": "white",
"bgcolor": "green",
"className": "",
"icon": "",
"payload": "forceOn",
"payloadType": "str",
"topic": "",
"topicType": "str",
"x": 100,
"y": 480,
"wires": [
[
"c36d8266accd7860"
]
]
},
{
"id": "da607f611829c027",
"type": "ui_button",
"z": "c7fde6141e82d752",
"name": "手動關閉",
"group": "e5af8077ea7bc427",
"order": 5,
"width": 3,
"height": 1,
"passthru": false,
"label": "手動 OFF",
"tooltip": "",
"color": "",
"bgcolor": "red",
"className": "",
"icon": "",
"payload": "forceOff",
"payloadType": "str",
"topic": "",
"topicType": "str",
"x": 100,
"y": 520,
"wires": [
[
"c36d8266accd7860"
]
]
},
{
"id": "cb4161ada8aae76e",
"type": "ui_button",
"z": "c7fde6141e82d752",
"name": "自動模式",
"group": "e5af8077ea7bc427",
"order": 7,
"width": 3,
"height": 1,
"passthru": false,
"label": "自動模式",
"tooltip": "",
"color": "",
"bgcolor": "gray",
"className": "",
"icon": "",
"payload": "auto",
"payloadType": "str",
"topic": "",
"topicType": "str",
"x": 100,
"y": 560,
"wires": [
[
"c36d8266accd7860"
]
]
},
{
"id": "c36d8266accd7860",
"type": "function",
"z": "c7fde6141e82d752",
"name": "設定手動模式",
"func": "flow.set('manualOverride', msg.payload);\nnode.status({text:`模式切換為 ${msg.payload}`});\nreturn null;",
"outputs": 1,
"x": 280,
"y": 520,
"wires": [
[]
]
},
{
"id": "c48496254c3bb193",
"type": "inject",
"z": "c7fde6141e82d752",
"name": "啟動時載入設定",
"props": [],
"repeat": "",
"once": true,
"onceDelay": "0.5",
"topic": "",
"x": 140,
"y": 40,
"wires": [
[
"f4a48dbf26d3f4ac"
]
]
},
{
"id": "f4a48dbf26d3f4ac",
"type": "file in",
"z": "c7fde6141e82d752",
"name": "讀取 schedule.json",
"filename": "/data/schedule.json",
"format": "utf8",
"x": 330,
"y": 40,
"wires": [
[
"fc3b727c53522c13"
]
]
},
{
"id": "fc3b727c53522c13",
"type": "function",
"z": "c7fde6141e82d752",
"name": "初始化排程資料",
"func": "if (!msg.payload) return null;\ntry {\n let list = JSON.parse(msg.payload);\n flow.set('scheduleList', list);\n return {payload: list};\n} catch (err) {\n return null;\n}",
"outputs": 1,
"x": 560,
"y": 40,
"wires": [
[
"692e2f012e2919fe"
]
]
},
{
"id": "e5af8077ea7bc427",
"type": "ui_group",
"name": "控制模式與時間",
"tab": "b271b1b4b9c3e8c8",
"order": 2,
"disp": true,
"width": 11,
"collapse": false,
"className": ""
},
{
"id": "3b8f4b37e0b6b986",
"type": "ui_group",
"name": "每週排程設定",
"tab": "b271b1b4b9c3e8c8",
"order": 1,
"disp": true,
"width": 8,
"collapse": false
},
{
"id": "192c2b20bef1e71a",
"type": "mqtt-broker",
"name": "mqttgo",
"broker": "broker.mqttgo.io",
"port": "1883",
"clientid": "",
"autoConnect": true,
"usetls": false,
"protocolVersion": "4",
"keepalive": "60",
"cleansession": true,
"autoUnsubscribe": true,
"birthTopic": "",
"birthQos": "0",
"birthRetain": "false",
"birthPayload": "",
"birthMsg": {},
"closeTopic": "",
"closeQos": "0",
"closeRetain": "false",
"closePayload": "",
"closeMsg": {},
"willTopic": "",
"willQos": "0",
"willRetain": "false",
"willPayload": "",
"willMsg": {},
"userProps": "",
"sessionExpiry": ""
},
{
"id": "b271b1b4b9c3e8c8",
"type": "ui_tab",
"name": "時間控制",
"icon": "dashboard",
"order": 1
}
]




沒有留言:
張貼留言