【電子工作】Flask×OpenWeatherMap×Raspberry Piで作る!-Chart.js版-

ラズパイ

以前の記事「【電子工作】Flask×OpenWeatherMap×Raspberry Piで作る!室内温湿度 × 天気情報 × Matplotlib グラフ × CSVログのWebアプリ」の改良版です。

ここでは、Raspberry Pi と Python を使って、室内センサー(DHT20)+ OpenWeatherMap の外気情報 を組み合わせたリアルタイム Web ダッシュボードを作る方法を紹介しています。

グラフ表示には Matplotlib ではなく Chart.js に変更したことで、スマホでもキレイに見える「軽量で高速な Web グラフ表示」を可能にしました。

スポンサーリンク
スポンサーリンク

完成イメージ

  • 室内温度・湿度を Raspberry Pi(DHT20)から取得
  • 外気温度・湿度を OpenWeatherMap API から取得
  • データは 5 分ごとに自動更新
  • Web ブラウザで グラフ(温度/湿度)を描画
  • CSV ログへの保存も可能
完成イメージ

用意するもの

アイテム用途リンク
Raspberry Pi(3B/4B推奨)Webサーバ+温湿度取得Amazonで見る
Grove 温湿度センサー V2.0(DHT20)室内温湿度を取得Amazonで見る
ジャンパーワイヤーI2C接続Amazonで見る
Python(Flask / Matplotlib / smbus2)Webアプリ作成
OpenWeatherMap APIキー外気天気情報の取得(無料)

SeeedStudio Grove 温湿度センサーV2.0(DHT20)

今回使用する温湿度センサーです。千石電商の方が安い場合もありますので、そちらもご参照下さい。

OpenWeatherMapについては以下をご参照下さい。

>>>【Flask入門】外部API(天気情報)と連携してWebアプリを作ろう

Flask プロジェクト構成

weather_app/
│ app.py
│ .env(APIキーなど)
└── templates/
    └── index.html
└── static/
    └── favicon.ico

Flask(Python)側のコード

ここでは Chart.js に対応した app.py(動作確認済み) を使います。Matplotlib から乗り換えたことで、ページが軽くスムーズになります。

ソースコードはGithubに公開しています。ご参照下さい。

Chart.js-index.html-

ここでは、Chart.jsをメインに解説していきます。Pythonについては、前回の記事をご参照下さい。

<!doctype html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <title>天気 × 室内センサー(5分自動更新)</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
  <!-- 5分ごとにページを自動リロード -->
  <meta http-equiv="refresh" content="300">

  <!-- Chart.js -->
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>

  <style>
    body { font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Noto Sans JP", sans-serif; margin: 24px; }
    .wrap { max-width: 900px; margin: 0 auto; }
    header { margin-bottom: 16px; }
    form { display: flex; gap: 8px; align-items: center; margin-bottom: 16px; }
    input[type="text"] { padding: 8px 10px; font-size: 16px; flex: 1; }
    button { padding: 8px 12px; font-size: 16px; cursor: pointer; }
    .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 16px; }
    .card { border: 1px solid #ddd; border-radius: 12px; padding: 16px; background: #fff; }
    .muted { color: #666; font-size: 14px; }
    .error { color: #b00020; }
    .row   { margin: 6px 0; }

    .graph-wrap { text-align: center; margin-top: 24px; }
    .graph-wrap canvas {
      max-width: 100%;
      display: block;
      margin: 8px auto 24px;
    }
  </style>
</head>
<body>
<div class="wrap">
  <header>
    <h1>天気 × 室内センサー(5分自動更新)</h1>
    <p class="muted">OpenWeatherMapの天気と、Raspberry Pi の DHT20 室温・湿度を表示します。</p>
  </header>

  {% with messages = get_flashed_messages(with_categories=true) %}
    {% if messages %}
      <ul>
        {% for cat, msg in messages %}
          <li class="{{ cat }}">{{ msg }}</li>
        {% endfor %}
      </ul>
    {% endif %}
  {% endwith %}

  <form method="post" action="/">
    <input type="text" name="city" value="{{ city }}" placeholder="都市名を入力(例: Osaka, Tokyo, New York)">
    <button type="submit">都市を変更して即更新</button>
  </form>

  <div class="grid">
    <!-- 天気カード -->
    <section class="card">
      <h2>天気({{ weather.city if weather.ok else city }})</h2>
      {% if weather.ok %}
        <div class="row">説明:{{ weather.desc }}</div>
        <div class="row">気温:{{ weather.temp }} ℃(体感 {{ weather.feels }} ℃)</div>
        <div class="row">湿度:{{ weather.humidity }} %</div>
        <div class="row">風速:{{ weather.wind }} m/s</div>
        <img src="https://openweathermap.org/img/wn/{{ weather.icon }}@2x.png" alt="天気アイコン">
      {% else %}
        <div class="error">天気情報の取得に失敗:{{ weather.error }}</div>
      {% endif %}
    </section>

    <!-- センサーカード -->
    <section class="card">
      <h2>室内(DHT20)</h2>
      {% if sensor.ok %}
        <div class="row">温度:{{ sensor.temp_c }} ℃</div>
        <div class="row">湿度:{{ sensor.humidity }} %</div>
      {% else %}
        <div class="error">センサー読み取りに失敗または未接続。</div>
        {% if sensor.note %}<div class="muted">{{ sensor.note }}</div>{% endif %}
      {% endif %}
    </section>
  </div>

  <p class="muted" style="margin-top: 16px;">
    最終更新:{{ updated_at.strftime("%Y-%m-%d %H:%M:%S") if updated_at else "—" }} 
    次回更新予定:{{ next_at.strftime("%Y-%m-%d %H:%M:%S") if next_at else "—" }}(5分ごと)
  </p>

  <!-- グラフ全体のラッパー -->
  <div class="graph-wrap">
    <h2>温湿度グラフ(室内+外気)</h2>
    <p class="muted">
      データ件数:{{ history_len }} 件(間隔 {{ history_interval_sec }} 秒)<br>
      CSVログ:{{ csv_path }} に保存中
    </p>

    <h3>温度グラフ</h3>
    <!-- サイズを属性で固定(Chart.js に触らせない) -->
    <canvas id="tempChart" width="800" height="300"></canvas>

    <h3>湿度グラフ</h3>
    <canvas id="humChart" width="800" height="300"></canvas>
  </div>
</div> <!-- .wrap -->

<script>
async function loadHistoryAndDraw() {
  // Chart.js の自動リサイズを完全停止
  Chart.defaults.responsive = false;
  Chart.defaults.maintainAspectRatio = false;

  try {
    const resp = await fetch("/api/history");
    if (!resp.ok) {
      console.error("history API error", resp.status);
      return;
    }
    const data = await resp.json();
    console.log("history data", data);

    // 安全に配列を取り出し
    const labels     = Array.isArray(data.times)       ? data.times       : [];
    const indoorTemp = Array.isArray(data.indoor_temp) ? data.indoor_temp : [];
    const indoorHum  = Array.isArray(data.indoor_hum)  ? data.indoor_hum  : [];
    const owmTemp    = Array.isArray(data.owm_temp)    ? data.owm_temp    : [];
    const owmHum     = Array.isArray(data.owm_hum)     ? data.owm_hum     : [];

    const tempCanvas = document.getElementById("tempChart");
    const humCanvas  = document.getElementById("humChart");
    if (!tempCanvas || !humCanvas) {
      console.error("canvas要素が見つかりません");
      return;
    }

    // 念のため height/width を固定(異常値を上書き)
    tempCanvas.width  = 800;
    tempCanvas.height = 300;
    humCanvas.width   = 800;
    humCanvas.height  = 300;

    const safeLabels     = labels.length ? labels : ["No data"];
    const safeIndoorTemp = labels.length ? indoorTemp : [null];
    const safeOwmTemp    = labels.length ? owmTemp    : [null];
    const safeIndoorHum  = labels.length ? indoorHum  : [null];
    const safeOwmHum     = labels.length ? owmHum     : [null];

    const ctxTemp = tempCanvas.getContext("2d");
    const ctxHum  = humCanvas.getContext("2d");

    // 温度グラフ
    new Chart(ctxTemp, {
      type: "line",
      data: {
        labels: safeLabels,
        datasets: [
          {
            label: "室内温度 (°C)",
            data: safeIndoorTemp,
            tension: 0.2,
            spanGaps: true
          },
          {
            label: "外気温度 (°C)",
            data: safeOwmTemp,
            tension: 0.2,
            spanGaps: true
          }
        ]
      },
      options: {
        responsive: false,
        maintainAspectRatio: false,
        interaction: { mode: "index", intersect: false },
        scales: {
          x: { ticks: { maxTicksLimit: 8 } },
          y: { title: { display: true, text: "Temperature (°C)" } }
        }
      }
    });

    // 湿度グラフ
    new Chart(ctxHum, {
      type: "line",
      data: {
        labels: safeLabels,
        datasets: [
          {
            label: "室内湿度 (%)",
            data: safeIndoorHum,
            tension: 0.2,
            spanGaps: true
          },
          {
            label: "外気湿度 (%)",
            data: safeOwmHum,
            tension: 0.2,
            spanGaps: true
          }
        ]
      },
      options: {
        responsive: false,
        maintainAspectRatio: false,
        interaction: { mode: "index", intersect: false },
        scales: {
          x: { ticks: { maxTicksLimit: 8 } },
          y: {
            title: { display: true, text: "Humidity (%)" },
            min: 0,
            max: 100
          }
        }
      }
    });

  } catch (e) {
    console.error("履歴の取得に失敗しました", e);
  }
}

// DOM構築完了後に実行
document.addEventListener("DOMContentLoaded", function () {
  loadHistoryAndDraw();
});
</script>
</body>
</html>

Chart.js のポイント解説

  • Matplotlib より 圧倒的に軽い
  • スマホでも高解像度でグラフを描画
  • 画面サイズに応じてキレイにレンダリング

Chart.js のトラブル回避ポイント

Chart.js の「勝手に height を書き換える問題」を無効化
Chart.defaults.responsive = false;
Chart.defaults.maintainAspectRatio = false;
canvas の height 属性を固定(300px)
<canvas id="tempChart" width="800" height="300"></canvas>

height=3.3e+07px のようになるのを防いでいます。

まとめ

本記事では、Raspberry Pi を使って、

  • 室温・湿度を DHT20 から取得
  • 外気温を OpenWeatherMap API から取得
  • Flask で Web ダッシュボード化
  • Chart.js でリアルタイムグラフ表示
  • CSV ログ保存
  • カードレイアウトで見やすく整理

という 本格的な IoT Web アプリ を完成させました。

これはそのまま家庭用モニタや会社の環境監視にも使えるレベルの仕上がりです。

タイトルとURLをコピーしました