# 使用 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` 的使用者界面,如下圖所示:

透過這個界面,您可以開始創建自己的流程和集成不同的模塊,進行物聯網和其他自動化項目的開發。
## 安裝 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` 的設置:

```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`:

這樣,您就完成了在 `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和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` 開發板套件以支持您的設置。

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

#### 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一樣的畫面,就代表成功了

## Node-RED 中配置 MQTT 並訂閱 ESP32 發送資料
為了在 `Node-RED` 中接收來自 `ESP32` 的溫濕度數據,您需要首先配置 `MQTT` 並建立一個 `Dashboard`。請按照以下步驟進行操作:
### 安裝 Dashboard 套件
1.首先,安裝 Node-RED Dashboard 套件。這可以通過 Node-RED 的管理界面輕鬆完成。
1.在「管理面板」(Manage palette)中,搜索並安裝「node-red-dashboard」。


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

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

#### 第三步:拉取 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` 服務提供商,請確保根據提供商的要求設置相應的伺服器地址、端口號以及可能的用戶名和密碼。


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

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

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 節點以顯示。
拉完節點的圖如下

將展示如何在 Node-RED 中連接到一個公開的氣象資料 API,並抓取各地區的溫度等氣象資訊。
### 獲取 API Token
1. 首先,訪問 [氣象資料開放平臺](https://opendata.cwa.gov.tw/index) 以獲取所需的 `API TOKEN`。

### 設置 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` 舉一反三

下面為最終接線圖

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

### 範例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` 合在一起顯示,下圖為接線圖

部屬成功後查看`Dashboard`

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