概要
ミニPCを購入したのでそれを自宅サーバーとし、メインのデスクトップPCやミニPCの状態を計測して可視化してみました。
- Windowsのマシン状態(CPUやGPUの負荷・温度・消費電力など)をC#で計測する
- InfluxDBでログを収集する
- Grafanaを使って可視化する
という話をします。
実装サンプル
DockerでInfluxDBとGrafanaを起動する
1. Docker Desktopのインストール
Windows上でDockerを使うため、Docker Desktopをインストールします。
同時にWSL2の導入も行われます。
2. docker composeの用意
InfluxDB
は時系列データベースで、今回のような定期的に計測したデータを保管するサービスとして最適です。
計測したデータをInfluxDB
に流し、それをGrafana
というツールでグラフ化して描画することにします。
次の記事を参考にdocker-compose.yml
を構築しました。
version: "2"
services:
influxdb:
image: influxdb:2.7.1
container_name: influxdb-local
volumes:
- C:/docker/volume/influxdb/data:/var/lib/influxdb2
- C:/docker/volume/docker/influxdb2/config:/etc/influxdb2
ports:
- 8086:8086
environment:
- DOCKER_INFLUXDB_INIT_MODE=setup
- DOCKER_INFLUXDB_INIT_USERNAME=admin
- DOCKER_INFLUXDB_INIT_PASSWORD=password
- DOCKER_INFLUXDB_INIT_ORG=organization
- DOCKER_INFLUXDB_INIT_BUCKET=BUCKET
restart: always
grafana:
image: grafana/grafana-oss:9.5.5
container_name: grafana-local
ports:
- 8085:3000
user: "472"
volumes:
- C:/docker/volume/grafana/data:/var/lib/grafana
depends_on:
- influxdb
environment:
- GF_SERVER_ROOT_URL=http://localhost:8085
- GF_SECURITY_ADMIN_PASSWORD=admin
restart: always
environment
の部分で初期アカウント名や初期パスワードが指定できます。
volumes
の部分は実際にデータが書き込まれるディレクトリになるため、容量の空いている場所を指定しておくとよいでしょう。
準備ができたらdocker-compose up
でコンテナを起動します。
コンテナが正しく起動したら次のURLにブラウザでアクセスし、各システムが立ち上がっていることを確認します。
- InfluxDB : http://localhost:8086/
- Grafana: http://localhost:8085/
3. InfluxDBの準備
- InfluxDB : http://localhost:8086/
InfluxDBにアクセスするとログイン画面が表示されます。
- Username :
DOCKER_INFLUXDB_INIT_USERNAME
で指定したもの - Password:
DOCKER_INFLUXDB_INIT_PASSWORD
で指定したもの
上記のUsernameとPasswordでログインできるはずです。
3-1. Bucketを作る
ログインができたらデータを格納するためのBucket
を作成します。
Load Data
-> Buckets
-> + CREATE BUCKET
より作成できます。
-
Name
: Bucketの名前。C#からデータを流し込む時に使います。 -
Delete Data
: データの保存期間。古いデータを自動で消してほしいならOLDER THAN
を指定。
3-2. API Tokenの作成
Bucketを作成したらアクセスするためのAPI Tokenを生成します。
Load Data
-> API TOKENS
-> GENERATE API TOKEN
-> Custom API Token
から作成できます。
アクセス範囲として、「3-1」で作成したBucketに対するWrite権を指定します。それ以上の権限は今回必要ありません。
API Tokenが作成できたらこれをメモしておきます。あとで使います。
C#でPCの状態を計測してInfluxDBに送る
続いてC#でPC状態を計測し、InfluxDBへ送れるようにします。
4. OpenHardwareMonitorLibを準備する
C#でWindowsの状態を計測するにはOpenHardwareMonitor
が使えます。
上記リポジトリをCloneして手元でビルドするか、ビルドファイルをダウンロードし、OpenHardwareMonitorLib.dllを抜き取っておきます。
※ OpenHardwareMonitorでマシン状態を取得する場合、いくつかのパラメーターは管理者権限が必要になります。
5. C#プロジェクトを.NET Framework4.8で作る
OpenHardwareMonitorLib
は.NET Framework向けのライブラリであるため、.NET Framework 4.8でC#プロジェクトを作成します。
- 今回は「PcMonitor」というプロジェクト名にした
- 「Windowsアプリケーション」で作成しているが、「コンソールアプリケーション」や「Windows Forms」でも可
プロジェクトができたら、 プロジェクトの参照にさきほどのOpenHardwareMonitorLib.dll
を追加しておきます。
<ItemGroup>
<Reference Include="OpenHardwareMonitorLib">
<HintPath> <!--OpenHardwareMonitorLib.dllへのPathをここに --></HintPath>
</Reference>
</ItemGroup>
6. 計測値を保存するデータ構造を用意する
OpenHardwareMonitor
で取得したデータを一時保存するデータ構造を定義します。
今回は次の情報を取得することにします。
- CPU
- パッケージ温度
- 消費電力
- 使用率
- GPU
- コア温度
- 消費電力
- 使用率
- メモリ
- 使用率
これらを格納するデータ構造を定義します。
using System;
namespace PcMonitor.Protocol
{
// マシン情報
[Serializable]
public class MachineInfo
{
public CpuInfo Cpu;
public MemoryInfo Memory;
public GpuInfo Gpu;
public MachineInfo(CpuInfo cpu, MemoryInfo memory, GpuInfo gpu)
{
Cpu = cpu;
Memory = memory;
Gpu = gpu;
}
public override string ToString()
{
return $"{nameof(Cpu)}: {Cpu}, {nameof(Memory)}: {Memory}, {nameof(Gpu)}: {Gpu}";
}
}
// CPU状態
[Serializable]
public class CpuInfo
{
public string Name;
public Element Temperature;
public Element Load;
public Element Watt;
public CpuInfo(string name, Element temperature, Element load, Element watt)
{
Name = name;
Temperature = temperature;
Load = load;
Watt = watt;
}
public override string ToString()
{
return $"{nameof(Name)}: {Name}, {nameof(Temperature)}: {Temperature}, {nameof(Load)}: {Load}, {nameof(Watt)}: {Watt}";
}
}
// メモリ状態
[Serializable]
public class MemoryInfo
{
public string Name;
public Element Load;
public MemoryInfo(string name, Element load)
{
Name = name;
Load = load;
}
public override string ToString()
{
return $"{nameof(Name)}: {Name}, {nameof(Load)}: {Load}";
}
}
// GPU状態
[Serializable]
public class GpuInfo
{
public string Name;
public Element Temperature;
public Element Load;
public Element Watt;
public GpuInfo(string name, Element temperature, Element load, Element watt)
{
Name = name;
Temperature = temperature;
Load = load;
Watt = watt;
}
public override string ToString()
{
return $"{nameof(Name)}: {Name}, {nameof(Temperature)}: {Temperature}, {nameof(Load)}: {Load}, {nameof(Watt)}: {Watt}";
}
}
// 各要素の数値
[Serializable]
public class Element
{
public string Type;
public string Target;
public float? Max;
public float? Current;
public Element(string type, string target, float? max, float? current)
{
Type = type;
Target = target;
Max = max;
Current = current;
}
public override string ToString()
{
return
$"{nameof(Type)}: {Type}, {nameof(Target)}: {Target}, {nameof(Max)}: {Max}, {nameof(Current)}: {Current}";
}
}
}
当初はjsonで書き出すことを想定していたのでそれを意識した構造になっています。Serializable
なのはその名残です。
このあたりのデータ構造は自分で扱いやすいように定義すればOKです。
7. OpenHardwareMonitorでデータを取得する
OpenHardwareMonitor
を用いてマシン情報を取得できるようにします。
基本的な使い方は次です。
-
OpenHardwareMonitor.Hardware.Computer
を作成 -
Computer.Open()
を呼び出す - 必要な
IHardware
を取得する -
IHardware
から各種数値を取得する
using System;
using System.Linq;
using OpenHardwareMonitor.Hardware;
using PcMonitor.Protocol;
namespace PcMonitor
{
// マシン状態の取得機構
public sealed class HardwareObserver : IDisposable
{
private readonly Computer _computer = new();
private readonly IHardware? _cpu;
private readonly IHardware? _memory;
private readonly IHardware? _gpu;
public HardwareObserver()
{
// OpenHardwareMonitorの初期化
_computer.Open();
// CPU, メモリ, GPUの情報を取得する
_computer.CPUEnabled = true;
_computer.RAMEnabled = true;
_computer.GPUEnabled = true;
// Computer.Hardwareに取得したデバイス情報が詰まっている
// ここからCPU, メモリ, GPUのデバイス情報を抜き取っておく
foreach (var hardware in _computer.Hardware)
{
switch (hardware.HardwareType)
{
case HardwareType.CPU:
_cpu = hardware;
break;
case HardwareType.RAM:
_memory = hardware;
break;
case HardwareType.GpuNvidia:
case HardwareType.GpuAti:
_gpu = hardware;
break;
}
}
}
/// <summary>
/// マシン状態を取得する
/// </summary>
public MachineInfo GetMachineInfo()
{
// 最新の状態に更新する
_cpu?.Update();
_memory?.Update();
_gpu?.Update();
var cpuInfo = GetCpuInfo();
var memoryInfo = GetMemoryInfo();
var gpuInfo = GetGpuInfo();
return new MachineInfo(cpuInfo, memoryInfo, gpuInfo);
}
/// <summary>
/// GPU状態の取得
/// </summary>
private GpuInfo? GetGpuInfo()
{
if (_gpu == null) return null;
// GPU温度
var gpuTemperature = _gpu.Sensors.FirstOrDefault(sensor =>
sensor.SensorType == SensorType.Temperature && sensor.Name == "GPU Core");
// 負荷
var gpuLoad = _gpu.Sensors.FirstOrDefault(sensor => sensor.SensorType == SensorType.Load
&& sensor.Name == "GPU Core");
// 消費電力
var gpuPower = _gpu.Sensors.FirstOrDefault(sensor => sensor.SensorType == SensorType.Power
&& sensor.Name == "GPU Power");
var gpuInfo = new GpuInfo(
_gpu.Name,
new Element("Temperature", gpuTemperature?.Name
, gpuTemperature?.Max, gpuTemperature?.Value),
new Element("Load", gpuLoad?.Name, gpuLoad?.Max, gpuLoad?.Value),
new Element("Watt", gpuPower?.Name, gpuPower?.Max, gpuPower?.Value)
);
return gpuInfo;
}
/// <summary>
/// メモリ情報の取得
/// </summary>
private MemoryInfo? GetMemoryInfo()
{
if (_memory == null) return null;
// メモリ使用率
var memoryLoad = _memory.Sensors.FirstOrDefault(sensor => sensor.SensorType == SensorType.Load);
var memoryInfo = new MemoryInfo(
_memory.Name,
new Element("Memory Load", _memory.Name, memoryLoad?.Max, memoryLoad?.Value)
);
return memoryInfo;
}
/// <summary>
/// CPU情報の取得
/// </summary>
private CpuInfo? GetCpuInfo()
{
if (_cpu == null) return null;
// CPU温度
var cpuTemperature = _cpu.Sensors.FirstOrDefault(sensor =>
sensor.SensorType == SensorType.Temperature && sensor.Name == "CPU Package");
// CPU負荷
var cpuLoad = _cpu.Sensors.FirstOrDefault(sensor => sensor.SensorType == SensorType.Load
&& sensor.Name == "CPU Total");
// CPU消費電力
var cpuWatt = _cpu.Sensors.FirstOrDefault(sensor => sensor.SensorType == SensorType.Power
&& sensor.Name == "CPU Package");
var cpuInfo = new CpuInfo(
_cpu.Name,
new Element("Temperature", cpuTemperature?.Name
, cpuTemperature?.Max, cpuTemperature?.Value),
new Element("Load", cpuLoad?.Name, cpuLoad?.Max, cpuLoad?.Value),
new Element("Watt", cpuWatt?.Name, cpuWatt?.Max, cpuWatt?.Value)
);
return cpuInfo;
}
public void Dispose()
{
_computer.Close();
}
}
}
using System;
using System.Threading;
using System.Threading.Tasks;
using PcMonitor;
// HardwareObserverの作成
using var hardwareObserver = new HardwareObserver();
// 1秒ごとにマシン情報を取得して表示する
while (true)
{
var machineInfo = hardwareObserver.GetMachineInfo();
Console.WriteLine(machineInfo);
await Task.Delay(TimeSpan.FromSeconds(1));
}
補足
実際にどのパラメーターが取れるのかは使用しているマシン構成によって変わります。
Open Hardware Monitorのexeを起動することで実際に計測できる数値を列挙できるので、これを参考に処理を組み立てるとよいでしょう。
8. InfluxDB.Clientを導入する
C#のInfluxDBクライアントとしてInfluxDB.Client
を使います。NuGetでプロジェクトへインストールします。
<ItemGroup>
<PackageReference Include="InfluxDB.Client" Version="4.18.0" />
</ItemGroup>
9. InfluxDBへの書き込み機構を作る
InfluxDBClient
を使ってInfluxDBへ計測データを書き込むようにします。
書き込み方法はいくつかありますが、今回はPOCO(Plain Old CLR Object)
を使う方法にしました。
using System;
using System.Threading.Tasks;
using InfluxDB.Client;
using InfluxDB.Client.Api.Domain;
using InfluxDB.Client.Core;
using PcMonitor.Protocol;
namespace PcMonitor
{
public sealed class InfluxDbSender : IDisposable
{
private readonly InfluxDBClient _influxDbClient;
private readonly string _bucket;
private readonly string _organization;
public InfluxDbSender(string url, string token, string bucket, string organization)
{
_influxDbClient = new InfluxDBClient(url, token);
_bucket = bucket;
_organization = organization;
}
// MachineInfoをInfluxDBへ書き込む
public async ValueTask SendMachineInfoAsync(MachineInfo machineInfo, CancellationToken ct)
{
try
{
// 非同期用のWriter取得
var writeApi = _influxDbClient.GetWriteApiAsync();
// CPU状態の書き込み
if (machineInfo.Cpu != null)
{
var cpu = new CPU
{
Time = DateTime.UtcNow,
Name = machineInfo.Cpu.Name,
Temperature = machineInfo.Cpu.Temperature.Current,
Load = machineInfo.Cpu.Load.Current,
Watt = machineInfo.Cpu.Watt.Current
};
await writeApi.WriteMeasurementAsync(cpu, WritePrecision.S, bucket: _bucket, org: _organization,
cancellationToken: ct);
}
// メモリ状態の書き込み
if (machineInfo.Memory != null)
{
var memory = new Memory
{
Time = DateTime.UtcNow,
Name = machineInfo.Memory.Name,
Load = machineInfo.Memory.Load.Current!
};
await writeApi.WriteMeasurementAsync(memory, WritePrecision.S, bucket: _bucket, org: _organization,
cancellationToken: ct);
}
// GPU状態の書き込み
if (machineInfo.Gpu != null)
{
var gpu = new GPU
{
Time = DateTime.UtcNow,
Name = machineInfo.Gpu.Name,
Temperature = machineInfo.Gpu.Temperature.Current!,
Load = machineInfo.Gpu.Load.Current!,
Watt = machineInfo.Gpu.Watt.Current!
};
await writeApi.WriteMeasurementAsync(gpu, WritePrecision.S, bucket: _bucket, org: _organization,
cancellationToken: ct);
}
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
public void Dispose()
{
_influxDbClient.Dispose();
}
}
}
using System;
using InfluxDB.Client.Core;
// POCO
// データベースの構造をクラス化したもの
namespace PcMonitor
{
[Measurement("CPU")]
public class CPU
{
[Column(IsTimestamp = true)] public DateTime Time { get; set; }
[Column("Name", IsTag = true)] public string Name { get; set; }
[Column("Temperature")] public double? Temperature { get; set; }
[Column("Load")] public double? Load { get; set; }
[Column("Watt")] public double? Watt { get; set; }
}
[Measurement("Memory")]
public class Memory
{
[Column(IsTimestamp = true)] public DateTime Time { get; set; }
[Column("Name", IsTag = true)] public string Name { get; set; }
[Column("Load")] public double? Load { get; set; }
}
[Measurement("GPU")]
public class GPU
{
[Column(IsTimestamp = true)] public DateTime Time { get; set; }
[Column("Name", IsTag = true)] public string Name { get; set; }
[Column("Temperature")] public double? Temperature { get; set; }
[Column("Load")] public double? Load { get; set; }
[Column("Watt")] public double? Watt { get; set; }
}
[Measurement("Network")]
public class Network
{
[Column(IsTimestamp = true)] public DateTime Time { get; set; }
[Column("Name", IsTag = true)] public string Name { get; set; }
[Column("SentByte")] public long Sent { get; set; }
[Column("ReceivedByte")] public long Received { get; set; }
}
}
using System;
using System.Threading;
using System.Threading.Tasks;
using PcMonitor;
// HardwareObserverの作成
using var hardwareObserver = new HardwareObserver();
// InfluxDbSenderの作成
using var influxDbSender = new InfluxDbSender(
url: "http://localhost:8086",
token: "3-2で作ったAPI Token",
bucket: "3-1で作ったBucketの名前",
organization: "DOCKER_INFLUXDB_INIT_ORGで指定したorganization名"
);
// 1秒ごとにマシン情報を取得してInfluxDBに送信
while (true)
{
var machineInfo = hardwareObserver.GetMachineInfo();
await influxDbSender.SendMachineInfoAsync(machineInfo, CancellationToken.None);
await Task.Delay(TimeSpan.FromSeconds(1));
}
10.ビルドして動かしてみる
準備ができたらビルドしてexeを起動してみます。注意点として管理者権限で起動しないとアクセスできない情報があります。
CPUの消費電力などは管理者権限でないと取得できません。
うまく動作していればInfluxDBのBucketにデータが書き込まれているはずです。
11.よしなに実装を整える
このままでも動作はしますが、Tokenなどがハードコードすぎるので外部ファイルから設定値を読み取って動くようにしてみました。
exeファイルがあるディレクトリと同じ場所にあるconfig.json
を読み込み、その設定値を用いて動作するようにしました。
Json.NETを使用しています。
using System;
namespace PcMonitor
{
[Serializable]
public class Configuration
{
public string? InfluxDbUrl = "http://localhost:8086";
public string? Token = "my-token";
public string? Organization = "my-org";
public string? Bucket = "my-bucket";
public int? IntervalMilliSeconds = 5000;
}
}
using System;
using System.IO;
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace PcMonitor.Sender
{
public static class Program
{
public static async Task Main()
{
try
{
// HardwareObserverの作成
using var hardwareObserver = new HardwareObserver();
// Configファイルの読み込み
var conf = GetConfiguration() ?? throw new Exception("Configuration is null");
// Configファイルを用いて初期化
var dbSender = new InfluxDbSender(conf.InfluxDbUrl, conf.Token, conf.Bucket, conf.Organization);
// 転送開始
await SendMachineInfoAsync(dbSender, hardwareObserver, conf);
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}
/// <summary>
/// 定期的にInfluxDBにマシン情報を送信する
/// </summary>
private static async Task SendMachineInfoAsync(InfluxDbSender influxDbSender,
HardwareObserver hardwareObserver,
Configuration conf)
{
var retryCount = 0;
while (true)
{
try
{
// マシン情報を取得して送信
var machineInfo = hardwareObserver.GetMachineInfo();
await influxDbSender.SendMachineInfoAsync(machineInfo, default);
retryCount = 0;
await Task.Delay(conf.IntervalMilliSeconds ?? 5000);
}
catch (Exception ex)
{
Console.WriteLine(ex);
// InfluxDBへの送信に失敗した場合はしばらく待ってリトライ
// リトライ間隔は徐々に伸ばす
retryCount++;
if (retryCount > 20) return;
await Task.Delay(TimeSpan.FromMinutes(retryCount));
}
}
}
/// <summary>
/// exeファイルがあるディレクトリにあるconfig.jsonを読み込む
/// </summary>
private static Configuration? GetConfiguration()
{
var appPath = System.Reflection.Assembly.GetExecutingAssembly().Location;
var appDir = Path.GetDirectoryName(appPath);
var path = Path.Combine(appDir, "config.json");
if (!File.Exists(path))
{
// 存在しないならデフォルト値で作る
var conf = new Configuration();
var j = JsonConvert.SerializeObject(conf);
File.WriteAllText(path, j);
return conf;
}
var json = File.ReadAllText(path);
return JsonConvert.DeserializeObject<Configuration>(json);
}
}
}
これも必要最低限の実装なので、必要に応じて改変してください。
Windows Forms
とかを使ってもよいかと。
Grafanaで可視化する
InfluxDBに書き込まれたデータをGrafanaから参照して表示できるようにしてみます。
(そもそもInfluxDB自体にグラフ化する機能があるのでGrafana不要といわれれば、そう)
12. InfluxDB側でRead用のAPI Tokenを作成する
InfluxDB側でGrafanaからデータを取得できるようにAPI Tokenを作成しておきます。
今回はRead
側の権限を付与しておきます。
13. GrafanaをInfluxDBに接続する
ローカルで建てたGrafanaにアクセスし、ログインします。初期IDとパスワードは両方ともadmin
です。
ログインができたらConnections
-> Connect data
より Influx DB
を選択します。
続いて各種パラメータを設定します。
- Query Language :
Flux
- HTTP-URL :
http://influxdb-local:8086
(Dockerのネットワークを指定) - InfluxDB Details-Organization :
DOCKER_INFLUXDB_INIT_ORGで指定したorganization名
- InfluxDB Details-Token :
11. で作成したAPI Token
- InfluxDB Details-Default
Bucket : Bucket名
設定が終わったらSave & test
を押します。InfluxDBへの接続テストが実行されるので、成功していることを確認してください。
14. GrafanaのDashboardsよりデータの可視化をする
DashboardのAdd Visualization
よりデータの可視化をします。
たとえばCPU温度を可視化したければ次のようなFluxクエリを指定します。
from(bucket: "DesktopPC")
|> range(start: v.timeRangeStart, stop: v.timeRangeStop)
|> filter(fn: (r) => r["_measurement"] == "CPU")
|> filter(fn: (r) => r["_field"] == "Temperature")
|> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: true)
|> yield(name: "mean")
15. よしなにグラフを揃えて完成
完成です!
感想
- InfluxDBとGrafanaの構築はDocker-Composeで一瞬だった
- OpenHardwareMonitorが
.NET Framework
でしか動かないこと、管理者権限が無いと情報が全部取れないところにハマった - いろいろ可視化すると面白くて楽しい
おまけ
ChatGPTにクエリを書かせると便利
InfluxDBに投げるように定義したPOCOですが、これをChatGPTにそのまま渡して指示すれば可視化用のFluxクエリを組み立ててくれます。
(そのままコピペでうまくいくこともあるが、多少の手直しは必要)
// これをそのままコピペしてChatGPTに食わせて、
// 「CPUのTemperatureをGrafanaでグラフ化するFluxクエリを作って」とか頼めば作ってくれる
[Measurement("CPU")]
public class CPU
{
[Column(IsTimestamp = true)] public DateTime Time { get; set; }
[Column("Name", IsTag = true)] public string Name { get; set; }
[Column("Temperature")] public double? Temperature { get; set; }
[Column("Load")] public double? Load { get; set; }
[Column("Watt")] public double? Watt { get; set; }
}
[Measurement("Memory")]
public class Memory
{
[Column(IsTimestamp = true)] public DateTime Time { get; set; }
[Column("Name", IsTag = true)] public string Name { get; set; }
[Column("Load")] public double? Load { get; set; }
}
[Measurement("GPU")]
public class GPU
{
[Column(IsTimestamp = true)] public DateTime Time { get; set; }
[Column("Name", IsTag = true)] public string Name { get; set; }
[Column("Temperature")] public double? Temperature { get; set; }
[Column("Load")] public double? Load { get; set; }
[Column("Watt")] public double? Watt { get; set; }
}
[Measurement("Network")]
public class Network
{
[Column(IsTimestamp = true)] public DateTime Time { get; set; }
[Column("Name", IsTag = true)] public string Name { get; set; }
[Column("SentByte")] public long Sent { get; set; }
[Column("ReceivedByte")] public long Received { get; set; }
}
電気料金を出してみた
CPUとGPUの「1秒おきのW数」は取れているのでこれから1時間当たりの平均値を求め「Wh」とし、そこに電気料金をかけることで1時間あたりのだいたいの電気料金を算出してみました。
(23時ごろにVRで遊びだしたので電気料金が一気に上がってるのがわかる)
// CPUの電気料金を計算
cpuCost = from(bucket: "MonitorDesktopPC")
|> range(start: v.timeRangeStart, stop: v.timeRangeStop)
|> filter(fn: (r) => r["_measurement"] == "CPU")
|> filter(fn: (r) => r["_field"] == "Watt")
// 1時間の平均からWhを求める
|> aggregateWindow(every: 1h, fn: mean, createEmpty: true)
|> map(fn: (r) => ({
_time: r._time,
// 第二段階の電気料金(1kWhあたり)をかける
CPU_ElectricityCost: (r._value / 1000.0) * 36.4
}))
// GPUの電気料金を計算
gpuCost = from(bucket: "MonitorDesktopPC")
|> range(start: v.timeRangeStart, stop: v.timeRangeStop)
|> filter(fn: (r) => r["_measurement"] == "GPU")
|> filter(fn: (r) => r["_field"] == "Watt")
|> aggregateWindow(every: 1h, fn: mean, createEmpty: true)
|> map(fn: (r) => ({
_time: r._time,
GPU_ElectricityCost: (r._value / 1000.0) * 36.4
}))
// CPUとGPUの結果を結合
join(tables: {cpu: cpuCost, gpu: gpuCost}, on: ["_time"])
|> map(fn: (r) => ({
_time: r._time,
TotalElectricityCost: r.CPU_ElectricityCost + r.GPU_ElectricityCost // 合計料金を計算
}))
|> yield(name: "TotalElectricityCost")