本文へスキップ
ysk-hack-log
戻る

温度ログをSQLite+EF Coreで蓄積し時系列グラフで振り返る(Raspberry Pi)【連載⑥】

「温度ログをSQLite+EF Coreで蓄積し時系列グラフで振り返る(Raspberry Pi)【連載⑥】」の記事アイキャッチ

前回(第5回) でリアルタイム表示ができました。でもアプリを止めると値は消えてしまいます。今回は SQLite+EF Core で温度を蓄積し、過去の推移を振り返れるようにします。

Table of contents

Open Table of contents

パッケージを追加

dotnet add package Microsoft.EntityFrameworkCore.Sqlite

モデルとDbContext

// Data/TemperatureReading.cs
public class TemperatureReading
{
    public int Id { get; set; }
    public DateTime TimestampUtc { get; set; }
    public double Celsius { get; set; }
}
// Data/AppDbContext.cs
using Microsoft.EntityFrameworkCore;

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
    public DbSet<TemperatureReading> Readings => Set<TemperatureReading>();
}

登録:Blazorでは IDbContextFactory を使う

BlazorのコンポーネントやバックグラウンドサービスはDbContextの寿命管理が難しく(DbContext はスレッドセーフでない・使い回すと壊れる)、Microsoftは AddDbContextFactory の利用を推奨しています。使うたびに短命なDbContextを生成する形にします。

// Program.cs
builder.Services.AddDbContextFactory<AppDbContext>(options =>
    options.UseSqlite("Data Source=temps.db"));

初回にDBファイルを作成します(起動時)。

// Program.cs(app.Run(); の前あたり)
using (var scope = app.Services.CreateScope())
{
    var factory = scope.ServiceProvider.GetRequiredService<IDbContextFactory<AppDbContext>>();
    using var db = factory.CreateDbContext();
    db.Database.EnsureCreated();
}

EnsureCreated() は手軽ですが、後からスキーマを変更できません。実運用では マイグレーション(dotnet ef migrations)が正解です。EF Coreの設計・N+1・性能改善は拙著『動かして学ぶ Entity Framework Core 実践入門』で深掘りしています。

バックグラウンドで保存する

第5回の TemperatureMonitor を拡張し、一定間隔でDBへ保存します(毎秒だと過剰なので10秒ごと)。

public sealed class TemperatureMonitor : BackgroundService
{
    private readonly TemperatureService _temp;
    private readonly IDbContextFactory<AppDbContext> _dbFactory;
    private int _tick;
    // (Current / history / Changed は第5回のまま)

    public TemperatureMonitor(TemperatureService temp,
                              IDbContextFactory<AppDbContext> dbFactory)
    {
        _temp = temp;
        _dbFactory = dbFactory;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
        while (await timer.WaitForNextTickAsync(stoppingToken))
        {
            Current = _temp.ReadCelsius();
            // …history更新・Changed発火(第5回のまま)…

            if (++_tick % 10 == 0) // 10秒ごとに保存
            {
                await using var db = await _dbFactory.CreateDbContextAsync(stoppingToken);
                db.Readings.Add(new TemperatureReading
                {
                    TimestampUtc = DateTime.UtcNow,
                    Celsius = Current
                });
                await db.SaveChangesAsync(stoppingToken);
            }
        }
    }
}

履歴を時系列グラフで表示

保存した直近のデータを読み、SVGの折れ線で描きます(第5回のグラフの応用)。

@* Components/Pages/History.razor *@
@page "/history"
@inject IDbContextFactory<AppDbContext> DbFactory
@using Microsoft.EntityFrameworkCore

<h1>温度の履歴</h1>

<svg width="600" height="200" style="border:1px solid #ccc">
    @if (!string.IsNullOrEmpty(_points))
    {
        <polyline points="@_points" fill="none" stroke="#006cac" stroke-width="2" />
    }
</svg>

@code {
    private string _points = "";

    protected override async Task OnInitializedAsync()
    {
        await using var db = await DbFactory.CreateDbContextAsync();
        var data = await db.Readings
            .OrderByDescending(r => r.TimestampUtc)
            .Take(100)
            .OrderBy(r => r.TimestampUtc)   // 古い順に並べ直す
            .Select(r => r.Celsius)
            .ToArrayAsync();

        _points = BuildPoints(data);
    }

    private static string BuildPoints(double[] data)
    {
        if (data.Length < 2) return "";
        const int w = 600, h = 200;
        double min = data.Min(), max = data.Max();
        double range = max - min < 0.1 ? 1 : max - min;
        return string.Join(" ", data.Select((v, i) =>
            $"{(double)i / (data.Length - 1) * w:F1},{h - (v - min) / range * h:F1}"));
    }
}

実機で起動してしばらく放置すると、/history に時間とともに蓄積された温度の推移が表示されます。アプリを再起動しても、temps.db にデータが残っているのが確認できます。

ブラウザ画面

つまずきポイント

まとめ

次回(第7回)は、MQTT連携。他のデバイスの値も集約し、しきい値を超えたらLEDで警告する、IoTダッシュボードらしい回にします。


前の記事:連載⑤ 温度をSignalRでライブグラフ表示する


この記事をシェア:

前の記事
温度をSignalRでライブグラフ表示する|Blazor Serverのリアルタイム更新【連載⑤】
次の記事
MQTT連携でIoTダッシュボードを作る|複数デバイスの値を集約(Raspberry Pi)【連載⑦】