使用 Node-RED 串接公開 API 與虛擬 ESP32 溫濕度感測器,透過 MQTT 實現數據傳輸並製作圖表展示

大致步驟

  1. 安裝和設置 Node-RED
    • 確保已安裝 Node-RED。如果未安裝,可訪問 Node-Red 官網 進行安裝。
    • 啟動 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. 整合數據並處理
    • 使用函數節點對從 MQTTAPI 收到的數據進行處理。
    • 轉換數據格式以適應後續的圖表顯示。
  7. 製作圖表
    • 添加 dashboard 節點(如:chart 節點)以製作圖表。
    • 配置圖表節點以顯示溫濕度數據。
  8. 部署並檢視結果
    • 部署流程。
    • Node-RED Dashboard 查看圖表,檢視實時數據。
  9. 調整和優化
    • 根據需要調整 MQTT 設置、函數邏輯或圖表配置。
    • 進行測試以確保系統穩定運行。

安裝和設置 Node-RED

為了開始使用 Node-RED,您首先需要安裝 Node.jsNode.js 是一個執行 JavaScript 代碼所必需的環境,它是 Node-RED 運行的基礎。按照以下步驟進行安裝:

安裝 Node.js

訪問 Node.js 官網 並下載適合您系統的版本。
完成下載後,按照指示安裝 Node.js

安裝 Node-RED

完成 Node.js 安裝後,您可以透過命令行界面安裝 Node-Red。請執行以下指令:

npm install -g --unsafe-perm node-red

這條指令將會全局安裝 Node-Red

啟動 Node-RED

安裝完成後,輸入以下指令以啟動 Node-RED

node-red

成功執行此指令後,Node-RED 會在您的本地機器上運行。

訪問 Node-RED 界面

啟動 Node-RED 後,打開瀏覽器並訪問 http://127.0.0.1:1880/。您將看到 Node-RED 的使用者界面,如下圖所示:

Node-RED 使用者界面

透過這個界面,您可以開始創建自己的流程和集成不同的模塊,進行物聯網和其他自動化項目的開發。

安裝 MQTT Broker

為了在 Node-RED 中使用 MQTT 功能,您需要先安裝一個 MQTT broker。這裡我們將使用 npm install node-red-contrib-aedes,這是一個流行的 MQTT broker 實現。您可以按照以下步驤進行安裝:

在您的命令行界面中,執行以下指令來安裝 npm install node-red-contrib-aedes

npm install node-red-contrib-aedes

安裝完成後,您可以在 Node-RED 中匯入下面的 JSON 配置以測試 broker 的設置:

1701150849101

[ { "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 安裝成功

這樣,您就完成了在 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進行。
  2. MQTT Subscriber:
    • 角色:Subscriber是消息的接收者。
    • 訂閱過程:SubscriberBroker訂閱一個或多個主題(topics)。這意味著它表明了對特定主題的消息感興趣。
    • 接收消息:當相應的主題有新消息時,Broker會將這些消息轉發給相應的Subscriber。在這個架構中,Broker充當中介,確保消息從發布者有效地傳遞給訂閱者。這種設計使得系統具有高度的解耦性和擴展性,非常適合於分散式和動態變化的環境,如物聯網。

配置虛擬及實體 ESP32 溫濕度感測器

虛擬 ESP32 溫濕度感測器

若想模擬 ESP32 溫濕度感測器的工作情形,您可以訪問 Wokwi 網站。Wokwi 是一個強大的在線模擬平台,它可以讓您模擬各種微控制器和電子元件,包括 ESP32DHT 溫濕度感測器。

為了開始模擬,請跟隨以下步驟:

  1. 訪問 wokwi 主頁。
  2. 在瀏覽器中打開這個已經撰寫好的範例:DHT+ESP32+MQTT。這個項目展示了如何使用 ESP32 讀取 DHT 感測器的溫濕度數據,並通過 MQTT 協議發送這些數據。

一旦您打開了範例項目,您將能夠查看源代碼、進行修改、並直接在瀏覽器中運行模擬。這是一種快速且有效的方式來測試和驗證您的 IoT 解決方案,而無需實際硬件。

實體 ESP32 溫濕度感測器

使用實體的ESP32微控制器和DHT11溫濕度感測器,通過WiFi連接和MQTT協議將感測器數據發布到互聯網。我們將提供硬件連接指南以及Arduino IDE的設置步驟。

硬件連接

請參考下圖以完成硬件連接:

實體 ESP32 溫濕度感測器

將ESP32和DHT11感測器連接如圖所示。確保連接正確,以確保感測器可以正常運作。

軟件環境設置

  1. 首先,下載並安裝Arduino IDE
  2. 打開Arduino IDE,並選擇 ESP32 DEV MODULE 作為目標開發板。
  3. 確保下載相關的 ESP32 開發板套件以支持您的設置。

下載開發版套件

如果在安裝過程中遇到缺少套件的錯誤,請複製套件名稱並在Arduino IDE中進行套件安裝。

如何下載套件

Arduino 程式碼

下面是 Arduino 程式碼,詳細解釋在程式碼中的每個部分:

#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

Node-RED 中配置 MQTT 並訂閱 ESP32 發送資料

為了在 Node-RED 中接收來自 ESP32 的溫濕度數據,您需要首先配置 MQTT 並建立一個 Dashboard。請按照以下步驟進行操作:

安裝 Dashboard 套件

1.首先,安裝 Node-RED Dashboard 套件。這可以通過 Node-RED 的管理界面輕鬆完成。
1.在「管理面板」(Manage palette)中,搜索並安裝「node-red-dashboard」。

安裝 Dashboard 套件步驟一

安裝 Dashboard 套件步驟二

配置 MQTT 輸入節點和 Dashboard

第一步:拉取節點

  1. 打開您的 Node-RED 流程編輯器,這是用於構建 IoT 應用程式的工具。
  2. 從節點列表中尋找並拉取一個名為 MQTT 的輸入節點。這個節點將用於接收來自您的 ESP32 微控制器的數據。

配置 MQTT 輸入節點和 Dashboard

第二步:配置 MQTT 輸入節點

  1. 點擊 MQTT 輸入節點以選中它。
  2. 在節點的屬性面板中,找到 "主題"(Topic)設定。這是 MQTT 主題,它用於標識和接收特定類型的數據。
  3. 設置 "主題" 的值,以匹配您的 ESP32 微控制器發布數據的主題。這個主題應該與您的 ESP32 上的程式碼中的主題一致,以確保數據能夠正確路由到這個節點。

PUBLICH更改

第三步:拉取 Dashboard 節點

  1. 從節點列表中找到和拉取與 Dashboard 有關的節點。Dashboard 節點用於在 Web 介面上創建和顯示用戶界面,以便顯示來自 IoT 設備的數據。

第四步:連接節點

  1. 現在,您需要將 MQTT 輸入節點和 Dashboard 節點連接在一起,以便將接收到的數據顯示在 Dashboard 上。
  2. Node-RED 畫布上,使用滑鼠將連接線從 MQTT 輸入節點拖動到 Dashboard 節點。這將建立數據流動的連接。
  3. 確保您在 Dashboard 節點中進一步配置該節點,以指定要在 Dashboard 上顯示的數據類型和方式。

注意事項

  1. 在配置 MQTT IN 節點時,確保主題設定與您的 ESP32 程式碼中的 MQTT 主題匹配,以確保數據能夠正確路由到 Node-RED
  2. 如果您使用的是公共 MQTT 服務提供商,請確保根據提供商的要求設置相應的伺服器地址、端口號以及可能的用戶名和密碼。

服務端更改
埠號更改

查看 Dashboard

完成配置並部署後,打開您的瀏覽器並訪問 http://127.0.0.1:1880/ui。如果您看到類似下面的界面,則表示您已經成功配置了 Dashboard 並正接收來自 ESP32 的數據。

Dashboard 成功畫面

範例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

首先前往氣象資料開放平臺,取得 TOKEN

氣象資料開放平臺API

  1. 設置 HTTP 請求節點:在 Node-RED 中,拉取一個 HTTP 請求節點。在這個節點的網址欄位中輸入以下 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
    記得將 Authorization 參數換成你的 Token
  2. 觸發請求並處理數據:透過選擇地區觸發 HTTP 請求,從 API 獲得數據。接著,將數據傳入 JSON 節點,然後傳入 MQTT OUT 節點。
  3. 接收並顯示數據:使用 MQTT IN 節點接收資料,並將其傳入 Dashboard 節點以顯示。

拉完節點的圖如下

1701152947137

將展示如何在 Node-RED 中連接到一個公開的氣象資料 API,並抓取各地區的溫度等氣象資訊。

獲取 API Token

  1. 首先,訪問 氣象資料開放平臺 以獲取所需的 API TOKEN

氣象資料開放平臺API

設置 HTTP 請求節點

  1. Node-RED 中,拉取一個 HTTP 請求節點(http request)。
  2. 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)節點。
  2. 透過選擇地區觸發 HTTP 請求,從 API 獲取數據。
  3. 將數據傳遞到 JSON 節點,然後通過 MQTT 輸出節點發送。
  4. 使用 MQTT 輸入(MQTT IN)節點接收數據,並將其傳遞到 Dashboard 節點。

Dashboard 節點要解析傳入的資料,其他 Dashboard 舉一反三

Dashboard 節點要解析傳入的資料

下面為最終接線圖

Node-RED 流程設置

查看結果

部署流程後,您可以在 Node-RED Dashboard 上查看從 API 獲取的氣象數據。
透過這些步驟,您可以方便地在 Node-RED 中集成公開氣象 API,並將數據實時顯示在 Dashboard 上。

1701153385444

範例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

部屬成功後查看Dashboard

1701155895873

匯出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 } ]