🔥

ラズパイサーバーでOpen-Meteoからの天気情報を取得する【Raspberry pi】

に公開

Raspberry pi サーバーで天気情報を取得するプログラムに挑戦

この前raspberry piのcpu温度をモニターするだけのプログラムを書きました。そこに1日の天気を表示するプログラムを追記したいと思います。

またpython -m http.serverでサーバーを動かしていましたがFastAPIを使ってより簡単に機能を追加できるようにしたいと思います。

  1. FastAPIの実装
  2. cpu温度モニター実装
  3. Open-Meteoを用いた天気情報取得

1. FastAPI

インストール

まずはFastAPIとそのほかに必要なライブラリをまとめてインストールします。

sudo apt update
sudo apt upgrade -y
pip install "fastapi[all]"

Debian/Raspberry Pi OSには特有の「PEP 668」という保護機能があるみたいで次のようなエラーが出ました。

error: externally-managed-environment

× This environment is externally managed
╰─> To install Python packages system-wide, try apt install
    python3-xyz, where xyz is the package you are trying to
    install.
    
    If you wish to install a non-Debian-packaged Python package,
    create a virtual environment using python3 -m venv path/to/venv.
    Then use path/to/venv/bin/python and path/to/venv/bin/pip. Make
    sure you have python3-full installed.
    
    For more information visit http://rptl.io/venv

note: If you believe this is a mistake, please contact your Python installation or OS distribution provider. You can override this, at the risk of breaking your Python installation or OS, by passing --break-system-packages.
hint: See PEP 668 for the detailed specification.

ここに書いてある通り仮想環境を作り、再度インストールを実行します。

cd code/
python3 -m venv venv
source venv/bin/activate
(venv) pip install "fastapi[all]"

これでインストールが実行されました。ちなみに実行中cpu温度が55℃を超えました。

実行中の古いプログラムを終了する

python -m http.serverの実行を停止します。

#止めたいサービスを探す
systemctl list-units --type=service | grep python

#自動起動止める
sudo systemctl disable raspi-temp.service
sudo systemctl disable raspi-web.service

#再起動
sudo reboot

2. CPU温度モニター

インストールされたFastAPIを使ってサーバーを起動します。Javascriptから/api/tempにfetchするとget_temp()によってcpu温度が返ってきます。毎秒apiを叩いていますがリアルタイム性が高いかつローカル通信なので問題ないかなと思います。外部サービスでやったらアウトの可能性大です。

main.py
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from datetime import datetime

app = FastAPI()

# --- APIセクション ---
@app.get("/api/temp")
def get_temp():
    with open("/sys/class/thermal/thermal_zone0/temp", "r") as f:
        temp_raw = f.read()
    return {
        "temp": float(temp_raw) / 1000.0,
        "server_time": datetime.now().strftime("%H:%M:%S") 
    }

# --- 静的ファイルセクション ---
# 最後にこれを置くことで、"/api/..." 以外のリクエストはすべて public フォルダを探しに行くようになります
app.mount("/", StaticFiles(directory="public", html=True), name="static")
temp.js
const temp_value = document.getElementById('temp-value');
const temp_time  = document.getElementById('time-value');

function updateTemp() {
    //fast apiで記述したエンドポイントへfetch
    fetch('./api/temp')
        .then(res => {
            if (!res.ok) throw new Error('Network error');
            return res.json();
        })
        .then(data => {
            // 小数点第1位まで固定して表示(25.0のように表示される)
            temp_value.textContent = data.temp.toFixed(1);
            temp_time.textContent = data.server_time;
        })
        .catch(err => {
            console.error("温度取得失敗:", err);
            temp_value.textContent = "--.-"; // エラー時はハイフン表示にする
        });
}

// 1秒ごとに実行
setInterval(updateTemp, 1000);
// 最初の実行
updateTemp();

これでCPU温度モニターを実装できました。

3. Open-Meteoによる天気情報取得

天気情報を取得して表示するプログラムを書きます。最高気温、最低気温、降水確率、風速、UV指数、天気指数の6つを取得します。

今回使うのはOpen-MeteoのForecast APIです。気象モデルの解説ページによると日本でよく使われる JMA (Japan Meteorological Agency) は3時間おきに更新されるみたいです。また以下ように示されているいことから、APIを呼び出すタイミングは「3の倍数時 + 10分」 を起点にするといいみたいです。またデフォルトで7日間の予報が取れるみたいです。

If you need access to the most recent forecast, it's recommended to wait an additional 10 minutes after the forecast update has been applied.

参考:
https://open-meteo.com/en/docs/model-updates
https://open-meteo.com/en/docs

公式ドキュメントにはopenmeteo_requestsライブラリを使ったpythonサンプルコードがありました。このコードはpandasを用いた解析などより高度な処理をするために参考になりそうです。今回は天気情報を取得して表示するだけなのでこれらは使いません。

実装

取得したい情報を基にAPIのURLを作ります。これはOpen-Meteoの公式サイトで行えます。1時間ごとの予報と1日の予報など、情報の取得の仕方は様々です。場所は東京で以下の情報を取得します。

1日(Daily Weather Variables)

  • Weather code
  • Maximum Temperature (2 m)
  • Minimum Temperature (2 m)
  • Precipitation Hours
  • Precipitation Probability Max
  • Maximum Wind Speed (10 m)
  • UV index

URLは次のようになります。

https://api.open-meteo.com/v1/forecast?latitude=35.6895&longitude=139.6917&daily=weather_code,temperature_2m_max,temperature_2m_min,precipitation_probability_max,uv_index_max,wind_speed_10m_max,precipitation_hours&timezone=Asia%2FTokyo&timeformat=unixtime

requestsライブラリを使用してAPIを呼び出すと以下のようなjsonが返ってきました。

JSON
{
  "latitude": 35.7,
  "longitude": 139.6875,
  "generationtime_ms": 0.375747680664063,
  "utc_offset_seconds": 32400,
  "timezone": "Asia/Tokyo",
  "timezone_abbreviation": "GMT+9",
  "elevation": 40,
  "daily_units": {
    "time": "unixtime",
    "weather_code": "wmo code",
    "temperature_2m_max": "°C",
    "temperature_2m_min": "°C",
    "precipitation_probability_max": "%",
    "uv_index_max": "",
    "wind_speed_10m_max": "km/h",
    "precipitation_hours": "h"
  },
  "daily": {
    "time": [1769698800, 1769785200, 1769871600, 1769958000, 1770044400, 1770130800, 1770217200],
    "weather_code": [1, 0, 0, 2, 3, 3, 3],
    "temperature_2m_max": [7, 7.6, 9.1, 9.3, 8.2, 11, 12.8],
    "temperature_2m_min": [-1.2, -0.8, -1.6, -0.4, 1, 1, 3.6],
    "precipitation_probability_max": [0, 0, 0, 1, 3, 0, 0],
    "uv_index_max": [3.95, 4, 4, 3.95, 3.9, 3.5, 3.15],
    "wind_speed_10m_max": [8, 9.2, 4.7, 5, 20.6, 8, 4.1],
    "precipitation_hours": [0, 0, 0, 0, 0, 0, 0]
  }
}

たくさん情報があるので必要なデータだけをブラウザに渡す処理を実装します。Dailyから今日(または明日)のデータを探す。

# 現在の「時(hour)」を取得
now_dt = datetime.now()
# 20時以降ならインデックスを 1 (明日) に、それ以前なら 0 (今日) に設定
day_idx = 1 if now_dt.hour >= 20 else 0

新しく追加した天気取得コードの全文はこのようになりました。

main.pyの一部
LATITUDE  = 35.6895
LONGITUDE = 139.6917

WEATHER_URL = f"https://api.open-meteo.com/v1/forecast?latitude={LATITUDE}&longitude={LONGITUDE}&daily=weather_code,temperature_2m_max,temperature_2m_min,precipitation_probability_max,uv_index_max,wind_speed_10m_max,precipitation_hours&hourly=temperature_2m,relative_humidity_2m,weather_code,precipitation_probability,wind_speed_10m&timezone=Asia%2FTokyo&timeformat=unixtime"

@app.get("/api/weather")
def get_weather():
    try:
        response = requests.get(WEATHER_URL, timeout=10)
        response.raise_for_status()
        data = response.json()

        # --- ここでデータを整理(フロントが使いやすい形に) ---

        # Dailyデータから「今日」の分を取得 (index 0)
        # ※ 20時以降なら index 1 (明日) を使うロジックもここに入れられます
        daily = data["daily"]
        
        # フロントエンドが必要な情報だけを凝縮
        display_data = {
            "server_time": now_dt.strftime("%H:%M:%S"),
            "daily_summary": {
                "label": day_idx,
                "max_temp": daily["temperature_2m_max"][day_idx],
                "min_temp": daily["temperature_2m_min"][day_idx],
                "precip_hours": daily["precipitation_hours"][day_idx],
                "prob_max": daily["precipitation_probability_max"][day_idx],
                "wind_max": daily["wind_speed_10m_max"][day_idx],
                "uv": daily["uv_index_max"][day_idx],
                "code": daily["weather_code"][day_idx]
            }
        }
        return display_data
    except Exception as e:
        return {"status": "error", "message": str(e)}

このコードのよって/api/weatherにアクセスすると以下のjsonが返ってきます。

{
  "daily_summary": {
    "label": 0,
    "max_temp": 7,
    "min_temp": -1.2,
    "precip_hours": 0,
    "prob_max": 0,
    "wind_max": 8,
    "uv": 3.95,
    "code": 1
  }
}

これをJavaScriptで表示します。

script.jsの一部
const weather_icon = document.getElementById('weather-icon');
const weather_time  = document.getElementById('weather-time-value');
const max_temp = document.getElementById('t-max');
const min_temp = document.getElementById('t-min');
const prob = document.getElementById('prob');
const wind = document.getElementById('wind');
const uv = document.getElementById('uv');

function getWeatherIcon(code) {
    let emoji = "\u263A"; // デフォルト
    // Open-Meteo WMOコードに基づいた判定
    if (code === 0) {
        emoji = "\u2600"; // 快晴 ☀️
    } else if (code <= 3) {
        emoji = "\u{1F324}"; // 晴れ〜くもり 🌤️
    } else if (code === 45 || code === 48) {
        emoji = "\u2601"; // 霧(くもりアイコン) ☁️
    } else if ((code >= 51 && code <= 65) || (code >= 80 && code <= 82)) {
        emoji = "\u2614"; // 雨 ☔
    } else if ((code >= 71 && code <= 77) || (code >= 85 && code <= 86)) {
        emoji = "\u2744"; // 雪 ❄️
    } else if (code >= 95) {
        emoji = "\u26A1"; // 雷 ⚡
    }
    return emoji;
}

function updateWeather() {
    fetch('./api/weather')
        .then(res => {
            if (!res.ok) throw new Error('Network error');
            return res.json();
        })
        .then(data => {
            // weather_value.textContent = data.current.temp;
            weather_icon.textContent = getWeatherIcon(data.daily_summary.code);
            weather_time.textContent  = data.server_time;
            max_temp.textContent = `${data.daily_summary.max_temp}°C`;
            min_temp.textContent = `${data.daily_summary.min_temp}°C`;
            prob.textContent = `${data.daily_summary.prob_max}%`;
            wind.textContent = `${data.daily_summary.wind_max}km/h`;
            uv.textContent = data.daily_summary.uv;
        })
        .catch(err => {
            console.error("天気取得失敗:", err);
            weather_value.textContent = "--.-";
            weather_time.textContent  = "Error";
        });
}
// 最初の実行
updateWeather();

このように表示できました。風速の単位が間違っています正しくはkm/hです。

htmlとcssはホワイトボードにレイアウトを書いてgeminiに送信してひな形を作ってもらい少し編集しました。

geminiに送った写真

4. まとめ

天気はスマホで見れるので実用的ではないかもしれませんね。でもラズパイサーバーでFastAPIとOpen-Meteoを使えたのでうれしいです。

Discussion