事發總有原因,先來個前情提要:

因此我打算研究一下,怎麼把智慧插座用電量餵給 Prometheus,用 Grafana 看圖表,如此用 SQLite 收資料跟畫圖表的土砲服務就能功成身退了。

Prometheus 抓資料的原理是在 prometheus.yaml 設定檔加入一段,指定作業名稱跟抓資料的網址:

scrape_configs:
  - job_name: "hs300"
    static_configs:
      - targets: ["localhost:9300"]

而 localhost:9300/metrics 要吐回類似以下格式的資料:

/metrics 的資料格式一望便知,其中定義了名為 hs300_stats 的 Gauge (儀表,會上下波動的度量數字) ,而這個 Gauge 包含了六種資料,插座 1 到插座 6,每次存取時會顯示各插座的耗電量(以瓦為單位)。

Promethues 有提供官方建議的 C# 程式庫,寫出以上服務簡單到像吃豆腐,建個 Console 專案,用 dotnet add package prometheus-netdotnet add package TPLinkSmartDevice.NETCore" 安裝 Prometheus 及 TPLink 智慧插座通訊程式庫,50 行搞定:

using Prometheus;
using System.Diagnostics;
using TPLinkSmartDevices.Devices;

// 讀取設定,提供預設值
var host = Environment.GetEnvironmentVariable("HS300_DeviceIP") ?? throw new ArgumentNullException("HS300_DeviceIP", "請提供 HS300 裝置的 IP 位址");

var metricServer = new MetricServer(port: 9999); // 指定 Exposure Port
metricServer.Start();

Metrics.SuppressDefaultMetrics(); // 停用預設指標

// 建立自訂指標
var myGauge = Metrics.CreateGauge("hs300_stats", "HS300 插座耗電量(w)", new GaugeConfiguration
{
    LabelNames = new[] { "type" }
});

// 以背景執行方式更新指標
await Task.Run(async () =>
{
    var rand = new Random();
    while (true)
    {
        double[] data = new double[6];
        long waitMs = 10_000;
        try
        {
            Console.Write(DateTime.Now.ToString("MM-dd HH:mm:ss "));
            var sw = Stopwatch.StartNew();
            var hs300 = new TPLinkSmartStrip(host);
            for (int i = 0; i < 6; i++)
            {
                var powerData = hs300.ReadRealtimePowerData(i + 1);
                data[i] = powerData.Power;
                myGauge.WithLabels($"插座{i + 1}").Set(powerData.Power);
            }
            sw.Stop();
            waitMs = Math.Max(waitMs - (int)sw.ElapsedMilliseconds, 0);
            Console.WriteLine($"Read data: {string.Join(",", data)} ({sw.ElapsedMilliseconds:n0}ms)");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"錯誤: {ex.Message}");
        }
        await Task.Delay(TimeSpan.FromMilliseconds(waitMs));
    }
});

程式要包成 Docker Image 送到 Linux 跑容器,我順便溫習了 Dockerfile 建 Image 的技巧。Copilot 是好老師,生成了一個很專業的 Dockerfile,我不懂就問,學到一些 Docker Image 製作技巧,整理在後面。

# 使用 .NET 9 SDK 作為建置環境
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /src

# 複製專案檔並還原相依套件 (利用快取機制,csproj 檔沒變動時,不需要重複還原)
COPY hs300-exporter.csproj .
RUN dotnet restore

# 專案只有一個 Program.cs 檔案
# 有很多檔案時,可使用 COPY . . 指令並配合 .dockerignore 檔案排除不要複製的檔案
COPY Program.cs .
# --no-restore 跳過前面已做過的 Package 還原步驟,加速建置
# /p:PublishSingleFile=false 不打包成單一檔,執行前不需解壓,啟動速度較快
# /p:PublishTrimmed=false 不進行程式碼修剪,加快建置減少出錯風險
RUN dotnet publish -c Release -o /app/publish \
    --no-restore \
    /p:PublishSingleFile=false \
    /p:PublishTrimmed=false

# 使用 .NET 9 Runtime 作為執行環境(較小的 Image)
FROM mcr.microsoft.com/dotnet/runtime:9.0
WORKDIR /app

# 複製建置輸出
COPY --from=build /app/publish .

# 宣告使用 Port (Metadata 性質,不影響編譯或執行)
EXPOSE 9999

# 執行應用程式
ENTRYPOINT ["dotnet", "hs300-exporter.dll"]

# 建置與執行
# docker build -t hs300-exporter .
# docker save -o hs300-exporter.tar hs300-exporter
# scp .\hs300-exporter.tar user@remotehost:/path/to/

以下是這次學到的 .NET 編譯 Dockerfile 技巧:

  • 兩次 FROM 切分成 Build Stage 及 Runtime Stage,兩次用的基底不同,Build 要用含完整 SDK 的 Image (730MB),Runtime 只需用 .NET Runtime Image (190MB) 即可。
  • Dockerfile 編譯過程產生 7 個 Layer (不含兩次 FROM 帶入的基底):
    1. WORKDIR /src 建立工作目錄
    2. COPY hs300-exporter.csproj . 複製專案檔
    3. RUN dotnet restore 執行套件還原
    4. COPY Program.cs . 複製原始碼檔案
    5. RUN dotnet publish... 發佈檔案
    6. WORKDIR /app 建立工作目錄
    7. COPY --from=build /app/publish . 複製編譯結果
      (註:EXPOSE 及 ENTRYPOINT 為 Metadata 不產生 Layer)
  • docker build 最終產生的 Image 只會包含 .NET Runtime Image 及 6, 7 Layer
  • 先 COPY .csproj、dotnet restore,之後 dotnet publish --no-restore 的原因是,Layer 可被 Cache,若 .csproj 沒變動,編譯時 1 到 3 層可以用快取內容,不需要重新執行。
  • /p:PublishSingleFile=false、/p:PublishTrimmed=false 有其考慮,但預設沒加註解(學長說:這不是常識嗎? XD),但問 Copilot 才知答案。

以上拆 .csproj 跟 dotnet restore 善用 Layer Cache 加速編譯的小訣竅官方教學沒講,謝謝 AI 不藏私帶我飛~

在 Linux 上用 docker-compose.yaml 啟動 hs300-exporter 容器並將 9999 Port 對應到本機的 9300 Port,Prometheus 會依 prometheus.yaml 指示到 http://localhost:9300/metrics 定期讀資料並寫入資料庫,要查詢最新 12 小時到最近五年的資料都不是問題。(註:Prometheus 要配合設定資料保存年限)

services:
  hs300-exporter:
    image: hs300-exporter:latest
    container_name: hs300-exporter
    restart: unless-stopped
    ports:
      - "9300:9999"
    environment:
      - TZ=Asia/Taipei
      - HS300_DeviceIP=192.168.50.80
    network_mode: bridge

另外,我使用的 cAdvisor/Prometheus/Grafana 範例沒有設定 Prometheus 資料長期保存,我調整了 docker-compose.yml 設定:

  prometheus:
    container_name: prometheus
    image: prom/prometheus:latest
    network_mode: "host"
 #   ports:
 #     - "9090:9090"
    volumes: 
      - "./prometheus.yml:/etc/prometheus/prometheus.yml"
      - "./prometheus-data:/prometheus" 
    command:  
      - "--config.file=/etc/prometheus/prometheus.yml"
      - "--storage.tsdb.path=/prometheus"
      - "--storage.tsdb.retention.time=5y"  
    privileged: true
    depends_on:
      - cadvisor

設定妥當後,在 Granfana 新增一個 Dashboard,指定接入 hs300_stats 資料,透過 Label Filter 指定 type = '插座1' 到 '插座6' 新增六個 Query,得到六個插座瓦數的折線圖:

Grafana 的圖表精美,設定介面挺直覺,最重要的是它的功能非常完整,你想得到的需求一應俱全。

像是指定查詢區期、設定篩選條件、詳細分析檢視... 等都難不倒它,用來管插座算是大材小用,Prometheus + Grafana 不愧是當前 DevOps 監測平台的一哥。

此刻,我也算摸到了 DevOps 的邊,感覺又更專業了一點,但這塊東西很多,未來有機會再繼續深入。

Migrated smart socket power monitoring from custom project to Prometheus + Grafana for advanced, automated charting and data management.


Comments

# by Elan

我想要去監控冷氣、熱水器等高耗電家電的使用量,但是他們的電路通常是獨立的,好像沒有適當的解決方案可以監控

# by Jeffrey

to Elan, 有種是用勾表原理測單線電流的產品(關鍵字 WIFi 能源監控器 80A 鉗),但多半是搭配 App 使用,不確定能程式整合。

# by GregYu

to Elan, Tapo P110 / Tapo P115 兩個插頭可與 Alexa 和 Google Home 運作, 某方面來說,也支援 Home Assistant 通訊 可以從這方面入手 另外,也有專門的設備, 安裝在 配電箱 直接監控電力使用狀況 https://www.youtube.com/watch?v=3iMe2N_pv0o

# by yoyo

exporter 能不能發佈成Native AOT,就不用 .NET Runtime了

# by Felix

Tapo 的智慧插座,似乎都不支援冷氣的 220V 有能支援 220V 的智慧插座嗎? WiFi 或 Matter 通訊皆可

# by Jeffrey

to yoyo, 嘿,被爆雷了,後面會有一篇討論精簡版 Image 及 AOT。

# by Neo Yao

黑大自己寫監控真厲害,前年因夏月電費破萬後就從亞馬遜入手了電力監測系統emporia 8迴路,分析用電大部份都是在晚上冷氣,就改申請台電的時間電價(免費換數位表電),冬夏月用電平均每度快4.x元回到3.x元.(老舊定頻冷氣和電暖器真的耗電,平均800-1000W,但無奈裝潢限制換掉要大工程)

# by Jeffrey

to Neo Yao, 原來是 Emporia,之前看過有人分享但忘了關鍵字一直沒查到。監控用電,Sensor 這段還是要靠現有產品,聽你提起 Emporia 有點想入手了。

Post a comment