# 使用 Node-RED 串接公開 API 與虛擬 ESP32 溫濕度感測器,透過 MQTT 實現數據傳輸並製作圖表展示 ## 大致步驟 1. 安裝和設置 `Node-RED` * 確保已安裝 `Node-RED`。如果未安裝,可訪問 [Node-Red](https://nodered.org/) 官網 進行安裝。 * 啟動 `Node-RED` 並打開其 `Web` 界面。 2. 安裝 `MQTT Broker` * 如果你沒有現成的 `MQTT Broker`,可以安裝如 `Mosquitto`。 * 安裝後,確保 `Broker` 正在運行。 3. 配置虛擬 `ESP32` 溫濕度感測器或是實體 `ESP32` 溫濕度感測器 * 使用模擬器或開發環境模擬 `ESP32`,並編程讓它定期發送溫濕度數據。 * 確保數據通過 `MQTT` 協議發送到 `Broker`。 4. `Node-RED` 中配置 `MQTT` * 在 `Node-RED` 的流中,添加一個 `MQTT` 輸入節點。 * 配置 `MQTT` 輸入節點以連接到你的 `MQTT Broker`,並訂閱 `ESP32` 發送的主題。 5. 連接到公開 `API` * 添加一個 `HTTP` 請求節點以連接到所需的公開 `API`。 * 配置節點以定期請求數據。 6. 整合數據並處理 * 使用函數節點對從 `MQTT` 和 `API` 收到的數據進行處理。 * 轉換數據格式以適應後續的圖表顯示。 7. 製作圖表 * 添加 `dashboard` 節點(如:`chart` 節點)以製作圖表。 * 配置圖表節點以顯示溫濕度數據。 8. 部署並檢視結果 * 部署流程。 * 在 `Node-RED Dashboard` 查看圖表,檢視實時數據。 9. 調整和優化 * 根據需要調整 `MQTT` 設置、函數邏輯或圖表配置。 * 進行測試以確保系統穩定運行。 ## 安裝和設置 Node-RED 為了開始使用 `Node-RED`,您首先需要安裝 `Node.js`。`Node.js` 是一個執行 `JavaScript` 代碼所必需的環境,它是 `Node-RED` 運行的基礎。按照以下步驟進行安裝: ### 安裝 Node.js 訪問 [Node.js](https://nodejs.org/en/) 官網 並下載適合您系統的版本。 完成下載後,按照指示安裝 [Node.js](https://nodejs.org/en/)。 ### 安裝 Node-RED 完成 [Node.js](https://nodejs.org/en/) 安裝後,您可以透過命令行界面安裝 [Node-Red](https://nodered.org/)。請執行以下指令: ```shell= npm install -g --unsafe-perm node-red ``` 這條指令將會全局安裝 [Node-Red](https://nodered.org/)。 ### 啟動 Node-RED 安裝完成後,輸入以下指令以啟動 `Node-RED`: ```shell= node-red ``` 成功執行此指令後,`Node-RED` 會在您的本地機器上運行。 ### 訪問 Node-RED 界面 啟動 `Node-RED` 後,打開瀏覽器並訪問 [http://127.0.0.1:1880/](http://127.0.0.1:1880/)。您將看到 `Node-RED` 的使用者界面,如下圖所示: ![Node-RED 使用者界面](https://hackmd.io/_uploads/Hkn7tyGrp.jpg) 透過這個界面,您可以開始創建自己的流程和集成不同的模塊,進行物聯網和其他自動化項目的開發。 ## 安裝 MQTT Broker 為了在 `Node-RED` 中使用 `MQTT` 功能,您需要先安裝一個 `MQTT broker`。這裡我們將使用 [npm install node-red-contrib-aedes](https://flows.nodered.org/node/node-red-contrib-aedes),這是一個流行的 `MQTT broker` 實現。您可以按照以下步驤進行安裝: 在您的命令行界面中,執行以下指令來安裝 [npm install node-red-contrib-aedes](https://flows.nodered.org/node/node-red-contrib-aedes): ```shell= npm install node-red-contrib-aedes ``` 安裝完成後,您可以在 `Node-RED` 中匯入下面的 `JSON` 配置以測試 `broker` 的設置: ![1701150849101](https://hackmd.io/_uploads/r1qolZ7B6.jpg) ```json= [ { "id": "4752af02ad2b6936", "type": "tab", "label": "Flow 1", "disabled": false, "info": "", "env": [] }, { "id": "602db9fdcf17624e", "type": "mqtt out", "z": "4752af02ad2b6936", "name": "", "topic": "sensors/livingroom/temp", "qos": "", "retain": "", "respTopic": "", "contentType": "", "userProps": "", "correl": "", "expiry": "", "broker": "e9d558a0a2f1767d", "x": 490, "y": 220, "wires": [] }, { "id": "11c103420d64f70c", "type": "inject", "z": "4752af02ad2b6936", "name": "", "repeat": "", "crontab": "", "once": false, "topic": "", "payload": "22", "payloadType": "num", "x": 310, "y": 220, "wires": [ [ "602db9fdcf17624e" ] ] }, { "id": "81bd7474e62ab78c", "type": "mqtt in", "z": "4752af02ad2b6936", "name": "", "topic": "sensors/livingroom/temp", "qos": "2", "datatype": "auto-detect", "broker": "e9d558a0a2f1767d", "nl": false, "rap": false, "inputs": 0, "x": 330, "y": 300, "wires": [ [ "674106a32fadc368" ] ] }, { "id": "674106a32fadc368", "type": "debug", "z": "4752af02ad2b6936", "name": "", "active": true, "console": "false", "complete": "false", "x": 530, "y": 300, "wires": [] }, { "id": "a5d723ab8d11088b", "type": "aedes broker", "z": "4752af02ad2b6936", "name": "", "mqtt_port": "1884", "mqtt_ws_bind": "port", "mqtt_ws_port": "", "mqtt_ws_path": "", "cert": "", "key": "", "certname": "", "keyname": "", "dburl": "", "usetls": false, "x": 310, "y": 120, "wires": [ [ "7caf4243938b959e" ], [ "d5f9898904f6d9b9" ] ] }, { "id": "7caf4243938b959e", "type": "debug", "z": "4752af02ad2b6936", "name": "debug 19", "active": false, "tosidebar": true, "console": false, "tostatus": false, "complete": "false", "statusVal": "", "statusType": "auto", "x": 520, "y": 80, "wires": [] }, { "id": "d5f9898904f6d9b9", "type": "debug", "z": "4752af02ad2b6936", "name": "debug 20", "active": false, "tosidebar": true, "console": false, "tostatus": false, "complete": "false", "statusVal": "", "statusType": "auto", "x": 520, "y": 140, "wires": [] }, { "id": "e9d558a0a2f1767d", "type": "mqtt-broker", "name": "localhost:1884", "broker": "localhost", "port": "1884", "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": "" } ] ``` 匯入並部署配置後,如果您看到以下界面,則表示您已成功安裝並配置了 `MQTT broker`: ![MQTT Broker 安裝成功](https://hackmd.io/_uploads/SkxjCRzHa.gif) 這樣,您就完成了在 `Node-RED` 中安裝 `MQTT broker` 的步驟,並可以開始使用 `MQTT` 進行消息傳輸。 ### 補充-MQTT是甚麼?MQTT Broker以及MQTT Subscriber各自扮演甚麼角色 `MQTT(Message Queuing Telemetry Transport)`是一種輕量級的消息傳輸協議,常用於物聯網(IoT)應用。在 `MQTT` 架構中,`「MQTT Broker」`和`「MQTT Subscriber」`扮演著關鍵角色,它們之間的關聯非常重要: 1. MQTT Broker: * 角色:`MQTT Broker`是一個消息服務器,負責接收、處理和轉發消息。 * 功能:`Broker`接收來自發布者(publishers)的消息,然後根據訂閱者(subscribers)的訂閱來分發這些消息。 * 中心節點:它是 `MQTT` 網絡中的中心節點,所有的消息交換都透過Broker進行。 1. MQTT Subscriber: * 角色:`Subscriber`是消息的接收者。 * 訂閱過程:`Subscriber`向`Broker`訂閱一個或多個主題(topics)。這意味著它表明了對特定主題的消息感興趣。 * 接收消息:當相應的主題有新消息時,`Broker`會將這些消息轉發給相應的`Subscriber`。在這個架構中,`Broker`充當中介,確保消息從發布者有效地傳遞給訂閱者。這種設計使得系統具有高度的解耦性和擴展性,非常適合於分散式和動態變化的環境,如物聯網。 ## 配置虛擬及實體 ESP32 溫濕度感測器 ### 虛擬 ESP32 溫濕度感測器 若想模擬 `ESP32` 溫濕度感測器的工作情形,您可以訪問 `Wokwi` 網站。`Wokwi` 是一個強大的在線模擬平台,它可以讓您模擬各種微控制器和電子元件,包括 `ESP32` 和 `DHT` 溫濕度感測器。 為了開始模擬,請跟隨以下步驟: 1. 訪問 [wokwi](https://wokwi.com/) 主頁。 1. 在瀏覽器中打開這個已經撰寫好的範例:[DHT+ESP32+MQTT](https://wokwi.com/projects/381995998774412289)。這個項目展示了如何使用 `ESP32` 讀取 `DHT` 感測器的溫濕度數據,並通過 `MQTT` 協議發送這些數據。 一旦您打開了範例項目,您將能夠查看源代碼、進行修改、並直接在瀏覽器中運行模擬。這是一種快速且有效的方式來測試和驗證您的 `IoT` 解決方案,而無需實際硬件。 ### 實體 ESP32 溫濕度感測器 使用實體的ESP32微控制器和DHT11溫濕度感測器,通過WiFi連接和MQTT協議將感測器數據發布到互聯網。我們將提供硬件連接指南以及Arduino IDE的設置步驟。 #### 硬件連接 請參考下圖以完成硬件連接: ![實體 ESP32 溫濕度感測器](https://hackmd.io/_uploads/SyrDInErp.jpg) 將ESP32和DHT11感測器連接如圖所示。確保連接正確,以確保感測器可以正常運作。 #### 軟件環境設置 1. 首先,下載並安裝[Arduino IDE](https://support.arduino.cc/hc/en-us/articles/360019833020-Download-and-install-Arduino-IDE)。 1. 打開[Arduino IDE](https://support.arduino.cc/hc/en-us/articles/360019833020-Download-and-install-Arduino-IDE),並選擇 `ESP32 DEV MODULE` 作為目標開發板。 1. 確保下載相關的 `ESP32` 開發板套件以支持您的設置。 ![下載開發版套件](https://hackmd.io/_uploads/SJ-wni4Ha.jpg) 如果在安裝過程中遇到缺少套件的錯誤,請複製套件名稱並在Arduino IDE中進行套件安裝。 ![如何下載套件](https://hackmd.io/_uploads/rkG0TjErp.jpg) #### Arduino 程式碼 下面是 `Arduino` 程式碼,詳細解釋在程式碼中的每個部分: ```csharp= #include <WiFi.h> #include <PubSubClient.h> #include <DHT.h> // 定義 DHT 傳感器引腳 #define DHTPIN 23 // 定義 DHT 類型 #define DHTTYPE DHT11 // 建立 DHT 物件 DHT dht(DHTPIN, DHTTYPE); const char* ssid = "CSIE-WLAN"; // WiFi SSID const char* password = "wificsie"; // WiFi 密碼 const char* mqtt_server = "test.mosquitto.org"; // MQTT 服務器地址 WiFiClient espClient; PubSubClient client(espClient); unsigned long lastMsg = 0; // 記錄上一次消息發送的時間 float temp = 0; // 溫度變量 float hum = 0; // 濕度變量 // 設置 WiFi 連接 void setup_wifi() { delay(10); Serial.println(); Serial.print("Connecting to "); Serial.println(ssid); WiFi.mode(WIFI_STA); WiFi.begin(ssid, password); // 等待 WiFi 連接 while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } randomSeed(micros()); Serial.println(""); Serial.println("WiFi connected"); Serial.println("IP address: "); Serial.println(WiFi.localIP()); } // MQTT 消息回調函數 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]); } } // 重連 MQTT 服務器 void reconnect() { while (!client.connected()) { Serial.print("Attempting MQTT connection..."); String clientId = "ESP32Client-"; clientId += String(random(0xffff), HEX); if (client.connect(clientId.c_str())) { Serial.println("Connected"); client.publish("/ThinkIOT/Publish", "Welcome"); client.subscribe("/ThinkIOT/Subscribe"); } else { Serial.print("failed, rc="); Serial.print(client.state()); Serial.println(" try again in 5 seconds"); delay(5000); } } } // 初始化設置 void setup() { pinMode(2, OUTPUT); Serial.begin(115200); setup_wifi(); client.setServer(mqtt_server, 1883); client.setCallback(callback); // dht.setup(DHT_PIN, DHTesp::DHT22); // 初始化 DHT22 dht.begin(); } // 主循環 void loop() { // 如果未連接 MQTT,則嘗試重新連接 if (!client.connected()) { reconnect(); } client.loop(); unsigned long now = millis(); if (now - lastMsg > 2000) { // 每 2 秒發布一次數據 lastMsg = now; // 讀取溫度和濕度 float temperature = dht.readTemperature(); float humidity = dht.readHumidity(); String temp = String(temperature, 2); client.publish("/Thinkitive/temp", temp.c_str()); // 發布溫度數據 String hum = String(humidity, 1); client.publish("/Thinkitive/hum", hum.c_str()); // 發布濕度數據 // 顯示溫度和濕度 Serial.print("溫度:"); Serial.print(temperature); Serial.println(" 攝氏度"); Serial.print("濕度:"); Serial.print(humidity); Serial.println(" %"); } } ``` 再經過後續的步驟,即可看到與連接虛擬ESP32一樣的畫面,就代表成功了 ![螢幕擷取畫面 2023-11-29 195518](https://hackmd.io/_uploads/Sy2LpjVBp.png) ## Node-RED 中配置 MQTT 並訂閱 ESP32 發送資料 為了在 `Node-RED` 中接收來自 `ESP32` 的溫濕度數據,您需要首先配置 `MQTT` 並建立一個 `Dashboard`。請按照以下步驟進行操作: ### 安裝 Dashboard 套件 1.首先,安裝 Node-RED Dashboard 套件。這可以通過 Node-RED 的管理界面輕鬆完成。 1.在「管理面板」(Manage palette)中,搜索並安裝「node-red-dashboard」。 ![安裝 Dashboard 套件步驟一](https://hackmd.io/_uploads/r19Nr-mB6.jpg) ![安裝 Dashboard 套件步驟二](https://hackmd.io/_uploads/BkZHH-QBp.jpg) ### 配置 MQTT 輸入節點和 Dashboard #### 第一步:拉取節點 1. 打開您的 `Node-RED` 流程編輯器,這是用於構建 `IoT` 應用程式的工具。 1. 從節點列表中尋找並拉取一個名為 `MQTT` 的輸入節點。這個節點將用於接收來自您的 `ESP32` 微控制器的數據。 ![配置 MQTT 輸入節點和 Dashboard](https://hackmd.io/_uploads/Hym6NbXHT.jpg) #### 第二步:配置 MQTT 輸入節點 1. 點擊 MQTT 輸入節點以選中它。 1. 在節點的屬性面板中,找到 "主題"(Topic)設定。這是 `MQTT` 主題,它用於標識和接收特定類型的數據。 1. 設置 "主題" 的值,以匹配您的 `ESP32` 微控制器發布數據的主題。這個主題應該與您的 `ESP32` 上的程式碼中的主題一致,以確保數據能夠正確路由到這個節點。 ![PUBLICH更改](https://hackmd.io/_uploads/BynHlh4S6.jpg) #### 第三步:拉取 Dashboard 節點 1. 從節點列表中找到和拉取與 `Dashboard` 有關的節點。`Dashboard` 節點用於在 `Web` 介面上創建和顯示用戶界面,以便顯示來自 `IoT` 設備的數據。 #### 第四步:連接節點 1. 現在,您需要將 `MQTT` 輸入節點和 `Dashboard` 節點連接在一起,以便將接收到的數據顯示在 `Dashboard` 上。 1. 在 `Node-RED `畫布上,使用滑鼠將連接線從 `MQTT` 輸入節點拖動到 `Dashboard` 節點。這將建立數據流動的連接。 1. 確保您在 `Dashboard` 節點中進一步配置該節點,以指定要在 `Dashboard` 上顯示的數據類型和方式。 #### 注意事項 1. 在配置 `MQTT IN` 節點時,確保主題設定與您的 `ESP32` 程式碼中的 `MQTT` 主題匹配,以確保數據能夠正確路由到 `Node-RED`。 1. 如果您使用的是公共 `MQTT` 服務提供商,請確保根據提供商的要求設置相應的伺服器地址、端口號以及可能的用戶名和密碼。 ![服務端更改](https://hackmd.io/_uploads/r11Sxn4HT.jpg) ![埠號更改](https://hackmd.io/_uploads/Bk7Hg2ESa.jpg) ### 查看 Dashboard 完成配置並部署後,打開您的瀏覽器並訪問 [http://127.0.0.1:1880/ui](http://127.0.0.1:1880/ui)。如果您看到類似下面的界面,則表示您已經成功配置了 `Dashboard` 並正接收來自 `ESP32` 的數據。 ![Dashboard 成功畫面](https://hackmd.io/_uploads/SJ_FHZmrT.jpg) ### 範例JSON ```json= [ { "id": "4752af02ad2b6936", "type": "tab", "label": "Flow 1", "disabled": false, "info": "", "env": [] }, { "id": "a5d723ab8d11088b", "type": "aedes broker", "z": "4752af02ad2b6936", "name": "", "mqtt_port": "1884", "mqtt_ws_bind": "port", "mqtt_ws_port": "", "mqtt_ws_path": "", "cert": "", "key": "", "certname": "", "keyname": "", "dburl": "", "usetls": false, "x": 310, "y": 120, "wires": [ [ "7caf4243938b959e" ], [ "d5f9898904f6d9b9" ] ] }, { "id": "7caf4243938b959e", "type": "debug", "z": "4752af02ad2b6936", "name": "debug 19", "active": false, "tosidebar": true, "console": false, "tostatus": false, "complete": "false", "statusVal": "", "statusType": "auto", "x": 520, "y": 80, "wires": [] }, { "id": "d5f9898904f6d9b9", "type": "debug", "z": "4752af02ad2b6936", "name": "debug 20", "active": false, "tosidebar": true, "console": false, "tostatus": false, "complete": "false", "statusVal": "", "statusType": "auto", "x": 520, "y": 140, "wires": [] }, { "id": "0182fafab959f3ff", "type": "mqtt in", "z": "4752af02ad2b6936", "name": "", "topic": "/Thinkitive/temp", "qos": "0", "datatype": "auto-detect", "broker": "2f00154a47567466", "nl": false, "rap": true, "rh": 0, "inputs": 0, "x": 300, "y": 220, "wires": [ [ "0de022206403765d" ] ] }, { "id": "2c2a8fa3f3e459bd", "type": "mqtt in", "z": "4752af02ad2b6936", "name": "", "topic": "/Thinkitive/hum", "qos": "0", "datatype": "auto-detect", "broker": "2f00154a47567466", "nl": false, "rap": true, "rh": 0, "inputs": 0, "x": 300, "y": 280, "wires": [ [ "07a3231ef07f68af" ] ] }, { "id": "0de022206403765d", "type": "ui_gauge", "z": "4752af02ad2b6936", "name": "TRMP", "group": "70d1d491db52e4fe", "order": 13, "width": 0, "height": 0, "gtype": "gage", "title": "ESP32溫度", "label": "", "format": "{{value}}", "min": 0, "max": "150", "colors": [ "#00b500", "#e6e600", "#ec2727" ], "seg1": "", "seg2": "", "diff": false, "className": "", "x": 510, "y": 220, "wires": [] }, { "id": "07a3231ef07f68af", "type": "ui_gauge", "z": "4752af02ad2b6936", "name": "", "group": "70d1d491db52e4fe", "order": 14, "width": 0, "height": 0, "gtype": "gage", "title": "ESP32濕度", "label": "", "format": "{{value}}", "min": 0, "max": "60", "colors": [ "#15cb15", "#e6e600", "#ca3838" ], "seg1": "", "seg2": "", "diff": false, "className": "", "x": 530, "y": 280, "wires": [] }, { "id": "2f00154a47567466", "type": "mqtt-broker", "name": "", "broker": "test.mosquitto.org", "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": "70d1d491db52e4fe", "type": "ui_group", "name": "esp32圖表", "tab": "2156c1cf7f043d72", "order": 2, "disp": true, "width": "6", "collapse": false, "className": "" }, { "id": "2156c1cf7f043d72", "type": "ui_tab", "name": "天氣圖表", "icon": "dashboard", "disabled": false, "hidden": false } ] ``` 透過這些步驟,您將能夠在 `Node-RED` 中實時監控來自 `ESP32` 的溫濕度數據,並透過一個視覺化的 `Dashboard` 展示這些信息。 ## 連接到公開 API 首先前往[氣象資料開放平臺](https://opendata.cwa.gov.tw/index),取得 `TOKEN` ![氣象資料開放平臺API](https://hackmd.io/_uploads/H1gE9yGST.jpg) 1. 設置 HTTP 請求節點:在 Node-RED 中,拉取一個 HTTP 請求節點。在這個節點的網址欄位中輸入以下 URL: ```bash= https://opendata.cwa.gov.tw/api/v1/rest/datastore/O-A0001-001?Authorization=替換為你的Token&limit=10&format=JSON&StationName={{payload}}&WeatherElement=Weather,Now,WindSpeed,AirTemperature,RelativeHumidity,AirPressure&GeoInfo=CountyName ``` 記得將 `Authorization` 參數換成你的 `Token`。 1. 觸發請求並處理數據:透過選擇地區觸發 HTTP 請求,從 API 獲得數據。接著,將數據傳入 JSON 節點,然後傳入 MQTT OUT 節點。 1. 接收並顯示數據:使用 MQTT IN 節點接收資料,並將其傳入 Dashboard 節點以顯示。 拉完節點的圖如下 ![1701152947137](https://hackmd.io/_uploads/HyMA_ZQHp.jpg) 將展示如何在 Node-RED 中連接到一個公開的氣象資料 API,並抓取各地區的溫度等氣象資訊。 ### 獲取 API Token 1. 首先,訪問 [氣象資料開放平臺](https://opendata.cwa.gov.tw/index) 以獲取所需的 `API TOKEN`。 ![氣象資料開放平臺API](https://hackmd.io/_uploads/H1gE9yGST.jpg) ### 設置 HTTP 請求節點 1. 在 `Node-RED` 中,拉取一個 `HTTP` 請求節點(http request)。 1. 在 `HTTP` 請求節點的 `URL` 欄位中,輸入以下網址,並將 `Authorization` 參數替換為您的 `Token`: ``` https://opendata.cwa.gov.tw/api/v1/rest/datastore/O-A0001-001?Authorization=替換為你的Token&limit=10&format=JSON&StationName={{payload}}&WeatherElement=Weather,Now,WindSpeed,AirTemperature,RelativeHumidity,AirPressure&GeoInfo=CountyName ``` ### 設置 Node-RED 流程 1. 拉取選單節點和 `JSON` 節點,以及 `MQTT` 輸出(MQTT OUT)節點。 1. 透過選擇地區觸發 `HTTP` 請求,從 `API` 獲取數據。 1. 將數據傳遞到 `JSON` 節點,然後通過 MQTT 輸出節點發送。 1. 使用 MQTT 輸入(MQTT IN)節點接收數據,並將其傳遞到 Dashboard 節點。 `Dashboard` 節點要解析傳入的資料,其他 `Dashboard` 舉一反三 ![Dashboard 節點要解析傳入的資料](https://hackmd.io/_uploads/rkxM-3EBa.jpg) 下面為最終接線圖 ![Node-RED 流程設置](https://hackmd.io/_uploads/HyMA_ZQHp.jpg) ### 查看結果 部署流程後,您可以在 `Node-RED Dashboard` 上查看從 `API` 獲取的氣象數據。 透過這些步驟,您可以方便地在 `Node-RED` 中集成公開氣象 `API`,並將數據實時顯示在 `Dashboard` 上。 ![1701153385444](https://hackmd.io/_uploads/SksOqb7Bp.jpg) ### 範例JSON ```json= [ { "id": "4752af02ad2b6936", "type": "tab", "label": "Flow 1", "disabled": false, "info": "", "env": [] }, { "id": "a5d723ab8d11088b", "type": "aedes broker", "z": "4752af02ad2b6936", "name": "", "mqtt_port": "1883", "mqtt_ws_bind": "port", "mqtt_ws_port": "", "mqtt_ws_path": "", "cert": "", "key": "", "certname": "", "keyname": "", "dburl": "", "usetls": false, "x": 330, "y": 220, "wires": [ [ "7caf4243938b959e" ], [ "d5f9898904f6d9b9" ] ] }, { "id": "7caf4243938b959e", "type": "debug", "z": "4752af02ad2b6936", "name": "debug 19", "active": false, "tosidebar": true, "console": false, "tostatus": false, "complete": "false", "statusVal": "", "statusType": "auto", "x": 540, "y": 180, "wires": [] }, { "id": "d5f9898904f6d9b9", "type": "debug", "z": "4752af02ad2b6936", "name": "debug 20", "active": false, "tosidebar": true, "console": false, "tostatus": false, "complete": "false", "statusVal": "", "statusType": "auto", "x": 540, "y": 240, "wires": [] }, { "id": "d3a027d3c2c251dd", "type": "mqtt out", "z": "4752af02ad2b6936", "name": "", "topic": "sensors/livingroom/temp", "qos": "", "retain": "", "respTopic": "", "contentType": "", "userProps": "", "correl": "", "expiry": "", "broker": "407a01e4.6b637", "x": 870, "y": 320, "wires": [] }, { "id": "f6f3f0550c2e3ee5", "type": "mqtt in", "z": "4752af02ad2b6936", "name": "", "topic": "sensors/livingroom/temp", "qos": "2", "datatype": "auto-detect", "broker": "407a01e4.6b637", "nl": false, "rap": false, "inputs": 0, "x": 870, "y": 380, "wires": [ [ "456b638badb6cfa3", "2650a0e7c32347e6", "c40492dd854991ca", "f4fdb780144bd0ef", "5dd6d4f8c2f31a7f" ] ] }, { "id": "456b638badb6cfa3", "type": "debug", "z": "4752af02ad2b6936", "name": "mqTT_In", "active": false, "tosidebar": true, "console": false, "tostatus": false, "complete": "payload", "targetType": "msg", "statusVal": "", "statusType": "auto", "x": 1140, "y": 380, "wires": [] }, { "id": "510543f154dc735e", "type": "ui_dropdown", "z": "4752af02ad2b6936", "name": "", "label": "選擇地區", "tooltip": "", "place": "點擊選擇", "group": "dcd3673081b04935", "order": 1, "width": 0, "height": 0, "passthru": true, "multiple": false, "options": [ { "label": "桃園", "value": "桃園", "type": "str" }, { "label": "苗栗", "value": "苗栗", "type": "str" }, { "label": "南投", "value": "南投", "type": "str" }, { "label": "彰化", "value": "埤頭", "type": "str" }, { "label": "台中", "value": "中坑", "type": "str" }, { "label": "雲林", "value": "斗六", "type": "str" }, { "label": "嘉義", "value": "水上", "type": "str" }, { "label": "台南", "value": "安南", "type": "str" }, { "label": "屏東", "value": "九如", "type": "str" }, { "label": "台東", "value": "延平", "type": "str" }, { "label": "花蓮", "value": "豐濱", "type": "str" }, { "label": "宜蘭", "value": "三星", "type": "str" }, { "label": "高雄", "value": "三民", "type": "str" }, { "label": "台北", "value": "平等", "type": "str" } ], "payload": "", "topic": "topic", "topicType": "msg", "className": "", "x": 300, "y": 320, "wires": [ [ "062dcad06a00311c" ] ] }, { "id": "062dcad06a00311c", "type": "http request", "z": "4752af02ad2b6936", "name": "", "method": "GET", "ret": "txt", "paytoqs": "ignore", "url": "https://opendata.cwa.gov.tw/api/v1/rest/datastore/O-A0001-001?Authorization=替換為你的Token&limit=10&format=JSON&StationName={{payload}}&WeatherElement=Weather,Now,WindSpeed,AirTemperature,RelativeHumidity,AirPressure&GeoInfo=CountyName", "tls": "", "persist": false, "proxy": "", "insecureHTTPParser": false, "authType": "", "senderr": false, "headers": [], "x": 490, "y": 320, "wires": [ [ "a8d2a1161cc2740f" ] ] }, { "id": "a8d2a1161cc2740f", "type": "json", "z": "4752af02ad2b6936", "name": "", "property": "payload", "action": "", "pretty": false, "x": 670, "y": 320, "wires": [ [ "d3a027d3c2c251dd" ] ] }, { "id": "2650a0e7c32347e6", "type": "ui_gauge", "z": "4752af02ad2b6936", "name": "", "group": "dcd3673081b04935", "order": 3, "width": "0", "height": "0", "gtype": "gage", "title": "氣溫", "label": "度", "format": "{{payload.records.Station[0].WeatherElement.AirTemperature}}", "min": 0, "max": "40", "colors": [ "#00b500", "#e6e600", "#ca3838" ], "seg1": "", "seg2": "", "diff": false, "className": "", "x": 1130, "y": 420, "wires": [] }, { "id": "c40492dd854991ca", "type": "ui_gauge", "z": "4752af02ad2b6936", "name": "", "group": "dcd3673081b04935", "order": 3, "width": 0, "height": 0, "gtype": "gage", "title": "風速", "label": "m/s", "format": "{{payload.records.Station[0].WeatherElement.WindSpeed}}", "min": 0, "max": "10", "colors": [ "#00b500", "#e6e600", "#ca3838" ], "seg1": "", "seg2": "", "diff": false, "className": "", "x": 1130, "y": 460, "wires": [] }, { "id": "f4fdb780144bd0ef", "type": "ui_gauge", "z": "4752af02ad2b6936", "name": "", "group": "dcd3673081b04935", "order": 3, "width": 0, "height": 0, "gtype": "gage", "title": "相對濕度", "label": "%", "format": "{{payload.records.Station[0].WeatherElement.RelativeHumidity}}", "min": "50", "max": "100", "colors": [ "#00b500", "#e6e600", "#ca3838" ], "seg1": "", "seg2": "", "diff": false, "className": "", "x": 1140, "y": 500, "wires": [] }, { "id": "5dd6d4f8c2f31a7f", "type": "ui_gauge", "z": "4752af02ad2b6936", "name": "", "group": "dcd3673081b04935", "order": 3, "width": 0, "height": 0, "gtype": "gage", "title": "空氣壓力", "label": "atm", "format": "{{payload.records.Station[0].WeatherElement.AirPressure}}", "min": "900", "max": "1100", "colors": [ "#00b500", "#e6e600", "#ca3838" ], "seg1": "", "seg2": "", "diff": false, "className": "", "x": 1140, "y": 540, "wires": [] }, { "id": "407a01e4.6b637", "type": "mqtt-broker", "name": "", "broker": "localhost", "port": "1883", "clientid": "", "autoConnect": true, "usetls": false, "protocolVersion": "4", "keepalive": "60", "cleansession": true, "autoUnsubscribe": true, "birthTopic": "", "birthQos": "0", "birthPayload": "", "birthMsg": {}, "closeTopic": "", "closePayload": "", "closeMsg": {}, "willTopic": "", "willQos": "0", "willPayload": "", "willMsg": {}, "userProps": "", "sessionExpiry": "" }, { "id": "dcd3673081b04935", "type": "ui_group", "name": "天氣", "tab": "2156c1cf7f043d72", "order": 1, "disp": true, "width": "6", "collapse": false, "className": "" }, { "id": "2156c1cf7f043d72", "type": "ui_tab", "name": "天氣圖表", "icon": "dashboard", "disabled": false, "hidden": false } ] ``` ## 兩個圖表一起顯示 將上述公開 `API` 以及 `ESP32` 合在一起顯示,下圖為接線圖 ![api以及esp32](https://hackmd.io/_uploads/Hk0EHzQrT.jpg) 部屬成功後查看`Dashboard` ![1701155895873](https://hackmd.io/_uploads/rywGafXST.jpg) ### 匯出JSON檔案 ```json= [ { "id": "4752af02ad2b6936", "type": "tab", "label": "Flow 1", "disabled": false, "info": "", "env": [] }, { "id": "d87bfba30f52a787", "type": "mqtt out", "z": "4752af02ad2b6936", "name": "", "topic": "sensors/livingroom/temp", "qos": "", "retain": "", "respTopic": "", "contentType": "", "userProps": "", "correl": "", "expiry": "", "broker": "407a01e4.6b637", "x": 1250, "y": 480, "wires": [] }, { "id": "46497999a0b9e240", "type": "mqtt in", "z": "4752af02ad2b6936", "name": "", "topic": "sensors/livingroom/temp", "qos": "2", "datatype": "auto-detect", "broker": "407a01e4.6b637", "nl": false, "rap": false, "inputs": 0, "x": 1250, "y": 540, "wires": [ [ "014d24a654cd8f4c", "912cb41e2b707cde", "fb13611218084b5d", "b9f9e902f028cef0", "2ece70af46bcc0cc" ] ] }, { "id": "014d24a654cd8f4c", "type": "debug", "z": "4752af02ad2b6936", "name": "mqTT_In", "active": false, "tosidebar": true, "console": false, "tostatus": false, "complete": "payload", "targetType": "msg", "statusVal": "", "statusType": "auto", "x": 1600, "y": 440, "wires": [] }, { "id": "8f23f6566f4d9758", "type": "aedes broker", "z": "4752af02ad2b6936", "name": "", "mqtt_port": 1883, "mqtt_ws_bind": "port", "mqtt_ws_port": "", "mqtt_ws_path": "", "cert": "", "key": "", "certname": "", "keyname": "", "dburl": "", "usetls": false, "x": 1230, "y": 340, "wires": [ [ "6292b4f38426117d" ], [ "4ae27ff101d2cb77" ] ] }, { "id": "6292b4f38426117d", "type": "debug", "z": "4752af02ad2b6936", "name": "debug 19", "active": false, "tosidebar": true, "console": false, "tostatus": false, "complete": "payload", "targetType": "msg", "statusVal": "", "statusType": "auto", "x": 1480, "y": 280, "wires": [] }, { "id": "4ae27ff101d2cb77", "type": "debug", "z": "4752af02ad2b6936", "name": "debug 20", "active": false, "tosidebar": true, "console": false, "tostatus": false, "complete": "false", "statusVal": "", "statusType": "auto", "x": 1480, "y": 400, "wires": [] }, { "id": "3bdc30ad14914136", "type": "ui_dropdown", "z": "4752af02ad2b6936", "name": "", "label": "選擇地區", "tooltip": "", "place": "點擊選擇", "group": "dcd3673081b04935", "order": 1, "width": 0, "height": 0, "passthru": true, "multiple": false, "options": [ { "label": "桃園", "value": "桃園", "type": "str" }, { "label": "苗栗", "value": "苗栗", "type": "str" }, { "label": "南投", "value": "南投", "type": "str" }, { "label": "彰化", "value": "埤頭", "type": "str" }, { "label": "台中", "value": "中坑", "type": "str" }, { "label": "雲林", "value": "斗六", "type": "str" }, { "label": "嘉義", "value": "水上", "type": "str" }, { "label": "台南", "value": "安南", "type": "str" }, { "label": "屏東", "value": "九如", "type": "str" }, { "label": "台東", "value": "延平", "type": "str" }, { "label": "花蓮", "value": "豐濱", "type": "str" }, { "label": "宜蘭", "value": "三星", "type": "str" }, { "label": "高雄", "value": "三民", "type": "str" }, { "label": "台北", "value": "平等", "type": "str" } ], "payload": "", "topic": "topic", "topicType": "msg", "className": "", "x": 500, "y": 480, "wires": [ [ "7224c1d51afb7b29", "ed9c5fc4953866d2" ] ] }, { "id": "ed9c5fc4953866d2", "type": "http request", "z": "4752af02ad2b6936", "name": "", "method": "GET", "ret": "txt", "paytoqs": "ignore", "url": "https://opendata.cwa.gov.tw/api/v1/rest/datastore/O-A0001-001?Authorization=your-token&limit=10&format=JSON&StationName={{payload}}&WeatherElement=Weather,Now,WindSpeed,AirTemperature,RelativeHumidity,AirPressure&GeoInfo=CountyName", "tls": "", "persist": false, "proxy": "", "insecureHTTPParser": false, "authType": "", "senderr": false, "headers": [], "x": 770, "y": 480, "wires": [ [ "f313640322f03c6b" ] ] }, { "id": "f313640322f03c6b", "type": "json", "z": "4752af02ad2b6936", "name": "", "property": "payload", "action": "", "pretty": false, "x": 950, "y": 480, "wires": [ [ "d87bfba30f52a787" ] ] }, { "id": "912cb41e2b707cde", "type": "ui_gauge", "z": "4752af02ad2b6936", "name": "", "group": "dcd3673081b04935", "order": 3, "width": "0", "height": "0", "gtype": "gage", "title": "氣溫", "label": "度", "format": "{{payload.records.Station[0].WeatherElement.AirTemperature}}", "min": 0, "max": "40", "colors": [ "#00b500", "#e6e600", "#ca3838" ], "seg1": "", "seg2": "", "diff": false, "className": "", "x": 1590, "y": 480, "wires": [] }, { "id": "fb13611218084b5d", "type": "ui_gauge", "z": "4752af02ad2b6936", "name": "", "group": "dcd3673081b04935", "order": 3, "width": 0, "height": 0, "gtype": "gage", "title": "風速", "label": "m/s", "format": "{{payload.records.Station[0].WeatherElement.WindSpeed}}", "min": 0, "max": "10", "colors": [ "#00b500", "#e6e600", "#ca3838" ], "seg1": "", "seg2": "", "diff": false, "className": "", "x": 1590, "y": 520, "wires": [] }, { "id": "b9f9e902f028cef0", "type": "ui_gauge", "z": "4752af02ad2b6936", "name": "", "group": "dcd3673081b04935", "order": 3, "width": 0, "height": 0, "gtype": "gage", "title": "相對濕度", "label": "%", "format": "{{payload.records.Station[0].WeatherElement.RelativeHumidity}}", "min": "50", "max": "100", "colors": [ "#00b500", "#e6e600", "#ca3838" ], "seg1": "", "seg2": "", "diff": false, "className": "", "x": 1600, "y": 560, "wires": [] }, { "id": "2ece70af46bcc0cc", "type": "ui_gauge", "z": "4752af02ad2b6936", "name": "", "group": "dcd3673081b04935", "order": 3, "width": 0, "height": 0, "gtype": "gage", "title": "空氣壓力", "label": "atm", "format": "{{payload.records.Station[0].WeatherElement.AirPressure}}", "min": "900", "max": "1100", "colors": [ "#00b500", "#e6e600", "#ca3838" ], "seg1": "", "seg2": "", "diff": false, "className": "", "x": 1600, "y": 600, "wires": [] }, { "id": "7224c1d51afb7b29", "type": "debug", "z": "4752af02ad2b6936", "name": "debug 22", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "false", "statusVal": "", "statusType": "auto", "x": 760, "y": 400, "wires": [] }, { "id": "e0f13a621b9c6c39", "type": "http in", "z": "4752af02ad2b6936", "name": "", "url": "/weather-app", "method": "get", "upload": false, "swaggerDoc": "", "x": 530, "y": 580, "wires": [ [ "a5155f909f85ea60" ] ] }, { "id": "a5155f909f85ea60", "type": "template", "z": "4752af02ad2b6936", "name": "JS版本氣象html", "field": "payload", "fieldType": "msg", "format": "handlebars", "syntax": "mustache", "template": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Weather Display</title>\n <style>\n body {\n font-family: 'Arial', sans-serif;\n margin: 0;\n padding: 0;\n background-color: #f4f4f4;\n color: #333;\n }\n\n .weather-container {\n margin-top: 20px;\n background-color: white;\n padding: 20px;\n border-radius: 8px;\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n width: 300px; /* 固定寬度 */\n }\n\n #stationSelect {\n padding: 8px;\n border-radius: 4px;\n border: 1px solid #ddd;\n margin-top: 20px;\n }\n\n h1, p {\n margin: 10px 0;\n }\n\n #city {\n font-size: 1.5em;\n color: #0275d8;\n }\n\n #weather {\n font-size: 1.2em;\n color: #5cb85c;\n }\n\n #temperature {\n font-size: 1.4em;\n color: #f0ad4e;\n }\n\n /* 響應式布局 */\n @media (max-width: 600px) {\n .weather-container {\n width: 90%;\n margin: 20px auto;\n }\n }\n\n </style>\n</head>\n<body>\n <div>\n <label for=\"stationSelect\">Choose a station:</label>\n <select id=\"stationSelect\">\n <option value=\"桃園\">桃園</option>\n <option value=\"苗栗\">苗栗</option>\n <option value=\"南投\">南投</option>\n <option value=\"埤頭\">彰化</option>\n <option value=\"中坑\">台中</option>\n <option value=\"安南\">台南</option>\n <!-- 更多選項 -->\n </select>\n </div>\n <div class=\"weather-container\">\n <h1 id=\"city\"></h1>\n <p id=\"weather\"></p>\n <p id=\"temperature\"></p>\n </div>\n <script>\n document.getElementById('stationSelect').addEventListener('change', (event) => {\n fetchWeatherData(event.target.value);\n });\n\n function fetchWeatherData(stationName) {\n const apiUrl = `https://opendata.cwa.gov.tw/api/v1/rest/datastore/O-A0001-001?Authorization=your-token&limit=10&format=JSON&StationName=${stationName}&WeatherElement=Weather,Now,WindSpeed,AirTemperature,RelativeHumidity,AirPressure&GeoInfo=CountyName`;\n\n fetch(apiUrl)\n .then(response => {\n if (!response.ok) {\n throw new Error('Network response was not ok');\n }\n return response.json();\n })\n .then(data => {\n console.log(data)\n updateWeatherDisplay(data);\n })\n .catch(error => {\n console.error('There has been a problem with your fetch operation:', error);\n });\n }\n\n function updateWeatherDisplay(data) {\n const temperature = data.records.Station[0]['WeatherElement']['AirTemperature'];\n document.getElementById('temperature').textContent = temperature;\n const weather = data.records.Station[0]['WeatherElement']['Weather'];\n document.getElementById('weather').textContent = weather;\n const city = data.records.Station[0]['GeoInfo']['CountyName'];\n document.getElementById('city').textContent = city;\n }\n\n // 初始化,載入頁面時自動加載預設站點的天氣數據\n fetchWeatherData(document.getElementById('stationSelect').value);\n\n </script>\n</body>\n</html>", "output": "str", "x": 780, "y": 580, "wires": [ [ "cacd3e3f65e86660" ] ] }, { "id": "cacd3e3f65e86660", "type": "http response", "z": "4752af02ad2b6936", "name": "", "statusCode": "", "headers": {}, "x": 950, "y": 580, "wires": [] }, { "id": "5974891f76b40263", "type": "mqtt in", "z": "4752af02ad2b6936", "name": "", "topic": "/Thinkitive/temp", "qos": "0", "datatype": "auto-detect", "broker": "2f00154a47567466", "nl": false, "rap": true, "rh": 0, "inputs": 0, "x": 1220, "y": 640, "wires": [ [ "8c8b14b4220552e7" ] ] }, { "id": "99b0ab903e745406", "type": "mqtt in", "z": "4752af02ad2b6936", "name": "", "topic": "/Thinkitive/hum", "qos": "0", "datatype": "auto-detect", "broker": "2f00154a47567466", "nl": false, "rap": true, "rh": 0, "inputs": 0, "x": 1220, "y": 700, "wires": [ [ "d3cad417a4b59315" ] ] }, { "id": "8c8b14b4220552e7", "type": "ui_gauge", "z": "4752af02ad2b6936", "name": "TRMP", "group": "70d1d491db52e4fe", "order": 13, "width": 0, "height": 0, "gtype": "gage", "title": "ESP32溫度", "label": "", "format": "{{value}}", "min": 0, "max": "150", "colors": [ "#00b500", "#e6e600", "#ec2727" ], "seg1": "", "seg2": "", "diff": false, "className": "", "x": 1430, "y": 640, "wires": [] }, { "id": "d3cad417a4b59315", "type": "ui_gauge", "z": "4752af02ad2b6936", "name": "", "group": "70d1d491db52e4fe", "order": 14, "width": 0, "height": 0, "gtype": "gage", "title": "ESP32濕度", "label": "", "format": "{{value}}", "min": 0, "max": "60", "colors": [ "#15cb15", "#e6e600", "#ca3838" ], "seg1": "", "seg2": "", "diff": false, "className": "", "x": 1450, "y": 700, "wires": [] }, { "id": "407a01e4.6b637", "type": "mqtt-broker", "name": "", "broker": "localhost", "port": "1883", "clientid": "", "autoConnect": true, "usetls": false, "protocolVersion": "4", "keepalive": "60", "cleansession": true, "autoUnsubscribe": true, "birthTopic": "", "birthQos": "0", "birthPayload": "", "birthMsg": {}, "closeTopic": "", "closePayload": "", "closeMsg": {}, "willTopic": "", "willQos": "0", "willPayload": "", "willMsg": {}, "userProps": "", "sessionExpiry": "" }, { "id": "dcd3673081b04935", "type": "ui_group", "name": "天氣", "tab": "2156c1cf7f043d72", "order": 1, "disp": true, "width": "6", "collapse": false, "className": "" }, { "id": "2f00154a47567466", "type": "mqtt-broker", "name": "", "broker": "test.mosquitto.org", "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": "70d1d491db52e4fe", "type": "ui_group", "name": "esp32圖表", "tab": "2156c1cf7f043d72", "order": 2, "disp": true, "width": "6", "collapse": false, "className": "" }, { "id": "2156c1cf7f043d72", "type": "ui_tab", "name": "天氣圖表", "icon": "dashboard", "disabled": false, "hidden": false } ] ```