LoginSignup
33
32

More than 5 years have passed since last update.

【Python】raspberry pi 置き時計に天気予報を追加した件

Last updated at Posted at 2019-03-18

前回の記事:【Python】raspberry pi を置き時計に仕立てる の続きです。

今どき日時表示しかできない置き時計なんて、つまらない。
そこで今回は OpenWeatherMap から気象予報データを取得して、現時点から 3 時間毎、翌日までの天気予報を日時の下に表示させるプログラムを作成しました。

勉強になったこと

  • クラスとコンストラクタ
  • 柔軟な配列
  • ディクショナリー(連想配列)
  • Tkinter における grid, pack, place 配置
  • json データにアクセス
  • Web API(OpenWeatherMap)

環境

  • Raspberry pi 3 Model B Rev 1.2
  • OS: Raspbian 9.6 (Stretch)
  • SD: Samsung microSD Card 32GB EVOPlus Class10 UHS-I
  • Display: Kuman 3.5inch HDMI Monitor Touch Screen Display Resolution (480*320 To 1920*1080)
  • Python 3.5.3

参考

【WebAPI】OpenWeatherMapで3時間ごとの天気を取りたい【json】

準備

OpenWeatherMap に登録(基本無料)

上記の参考ページに書いてある通りに登録して、API_KEY を取得します。
取得したらソースコード KEY に貼り付けます。
URL は参考ページからそのまま頂いたもの、ZIP はとりあえず神田岩本町にしてみました。

# OpenWeatherMap の情報
KEY = "********************************"
ZIP = "101-0032,JP"
URL = "http://api.openweathermap.org/data/2.5/forecast?zip={0}&units=metric&lang=ja&APPID={1}"

天気画像の作成

公式のアイコン もありましたがイマイチだったので ICOOON MONO から頂いたものを編集してそれっぽくしました。アイコンファイル名は公式と基本同じにしています。xxd.png が朝、xxn.png が夜のアイコンですが、太陽が月に変わるぐらいなので 03.png 以降の画像は共通化してます。

Screenshot_20190318.png

本題

メインフレームクラス

プログラムが起動すると、各パーツ(ウィジェット)を作成・画面に配置するためのメインフレームが初期化されます。

# MainFrame クラス
class MainFrame(ttk.Frame):
    # コンストラクタ
    def __init__(self, master=None, **kwargs):
        # 親クラスのコンストラクタを呼び出す
        super().__init__(master, **kwargs)

        # create_widgets を呼び出す
        self.create_widgets()

時計ウィジェットの作成

日時表示と天気予報地域表示用のラベルを作成します。
gridpack ではうまく配置できなかったので place で直接表示位置を指定しています。

  • grid:格子状(Excel 風)に配置
  • pack:縦または横方向へ一次元的に配置
  • place:位置を指定して配置

フレームを新たに作成してラベルをペタペタ貼り付けて、そのフレームを丸ごとメインフレームに grid で配置するイメージです。

# ウィジェットを作成
def create_widgets(self):
    # フレームを作成
    self.frame=Frame(self, bg="white", bd=0, height=460, relief="flat")

    # 時刻表示(配置は直接指定)
    self.wt=Label(self.frame, text="", bg="white", font=("", 160))
    self.wt.place(width=884, x=60, y=120)

    # 日付表示(配置は直接指定)
    self.wd=Label(self.frame, text="", bg="white", font=("", 60, "bold"))
    self.wd.place(width=744, x=160, y=60)

    # 地域表示(左寄せ)
    self.wp=Label(self.frame, text="", bg="white", fg="gray", font=("", 20, "bold"), anchor="w")
    self.wp.place(width=920, x=42, y=440)

    # フレームを配置
    self.frame.grid(row=0, column=0, columnspan=8, sticky="news")

天候アイコンを読み込む

準備していた天気画像を raspberry pi にあらかじめ転送しておきます。
今回は img ディレクトリに転送しました。

$ scp 01d.png 01n.png 02d.png 02n.png 03.png 04.png 09.png 10.png 11.png 13.png 50.png pi@192.168.X.X:/home/pi/python/img
pi@192.168.X.X's password:

プログラムが /home/pi/python/clock.py に配置してあるとして、
画像は絶対パスでロードしなければならない(autostart 自動起動設定になっている)ため、プログラム側が自分自身の絶対パス(/home/pi/python)を拾います。

ディクショナリーの初期化にて、絶対パス指定の画像をロードしています。
(というか、value に文字列や数値以外のデータを格納できることに驚きました)

# このスクリプトの絶対パス
self.scr_path = os.path.dirname(os.path.abspath(sys.argv[0]))

# 天候アイコン(ディクショナリ)
self.icon_dict={
    "01d":Image.open(self.scr_path + "/img/01d.png"), "01n":Image.open(self.scr_path + "/img/01n.png"),
    "02d":Image.open(self.scr_path + "/img/02d.png"), "02n":Image.open(self.scr_path + "/img/02n.png"),
    "03d":Image.open(self.scr_path + "/img/03.png"),  "03n":Image.open(self.scr_path + "/img/03.png"),
    "04d":Image.open(self.scr_path + "/img/04.png"),  "04n":Image.open(self.scr_path + "/img/04.png"),
    "09d":Image.open(self.scr_path + "/img/09.png"),  "09n":Image.open(self.scr_path + "/img/09.png"),
    "10d":Image.open(self.scr_path + "/img/10.png"),  "10n":Image.open(self.scr_path + "/img/10.png"),
    "11d":Image.open(self.scr_path + "/img/11.png"),  "11n":Image.open(self.scr_path + "/img/11.png"),
    "13d":Image.open(self.scr_path + "/img/13.png"),  "13n":Image.open(self.scr_path + "/img/13.png"),
    "50d":Image.open(self.scr_path + "/img/50.png"),  "50n":Image.open(self.scr_path + "/img/50.png")
}

# アイコンサイズを画面サイズにフィット(64x64)させる
for key, value in self.icon_dict.items():
    self.icon_dict[key]=self.icon_dict[key].resize((64, 64), Image.ANTIALIAS)
    self.icon_dict[key]=ImageTk.PhotoImage(self.icon_dict[key])

画像は 256x256 で作成したので、64x64 アンチエイリアスでフィットさせます。
これは PIL(Python Image Library)で実現していますが、ImageTk はインストールされていなかったため、ImportError になりました。この場合は apt-get でインストールします。

from PIL import Image, ImageTk
ImportError: cannot import name 'ImageTk'
$ sudo apt-get install python3-pil python3-pil.imagetk

天気予報の各パーツを配置

天気予報の表示は時間帯、天候、気温、降水量を 8x4 のテーブル配置にしました。
以下は天候アイコンのコードですが、初期化時は晴れマークを 8 列分並べたラベル配列を定義してメインフレームに配置しています。OpenWeatherMap からの情報を受け取ったら、ラベルの内容を更新していく動きになります。

# 天気予報(天候)
self.wwi=[
    Label(self, image=self.icon_dict["01d"], bg="white"),
    Label(self, image=self.icon_dict["01d"], bg="white"),
    Label(self, image=self.icon_dict["01d"], bg="white"),
    Label(self, image=self.icon_dict["01d"], bg="white"),
    Label(self, image=self.icon_dict["01d"], bg="white"),
    Label(self, image=self.icon_dict["01d"], bg="white"),
    Label(self, image=self.icon_dict["01d"], bg="white"),
    Label(self, image=self.icon_dict["01d"], bg="white")
]

# 天気予報(天候)を配置
for i in range(len(self.wwi)):
    self.wwi[i].grid(row=2, column=i, sticky="news")

フルスクリーン表示

天気予報を表示することで画面がそこそこ窮屈になりました。
最大化ではタスクバーとタイトルバーが画面上部に表示されてしまい、より窮屈に感じます。なのでフルスクリーンにして 3.5 inch の小型液晶画面を最大限活用することにしました。ただし、フルスクリーン表示にすると画面を閉じることができなくなってしまうので、閉じるボタンを画面右上に自作しました。

画面のリサイズは手動ではできませんが、プログラム実行時に配置が再計算されてズレることがあるので、一応念のため組み込んでいます。

# 閉じるボタン作成
btn=Button(root, text=" X ", font=('', 16), relief=FLAT, command=wm_close)

# 画面がリサイズされたとき
def change_size(event):
    # ボタンの位置を右上に
    btn.place(x=root.winfo_width() - 60, y=14)

# 画面のリサイズをバインドする
root.bind('<Configure>', change_size)

# メインウィンドウの最大化
#root.attributes("-zoom", "1")
root.attributes("-fullscreen", "1")

気象データを取得

URL をリクエストすると気象データを取得できます。
データは json で提供されるため、json パッケージで展開します。

# URL を作成して OpenWeatherMap に問い合わせを行う
url=URL.format(ZIP, KEY)
response=requests.get(url)
forecastData=json.loads(response.text)

# 結果が得られない場合は即時終了
if not ("list" in forecastData):
    print("error")
    return

気象データは基本的に 3h 毎の連続したレコードになっています。
なので、先頭から 8 レコード分(app.wwl 配列の個数分)を取得すれば目的は達成できます。forecastData を整形した item から key を指定して必要なデータを取り出します。

データを取り出して、表示が終わったら最後に地域情報を更新します。
地域情報(city)はリストの外側にあるので、forecastData から直接データを取り出します。

# 結果を 3 時間単位で取得
for item in forecastData["list"]:
    # 時間帯を 24 時間表記で表示
    forecastDatetime = datetime.fromtimestamp(item["dt"])
    app.wwl[count].configure(text=forecastDatetime.hour)

    # 気候をアイコンで表示
    app.wwi[count].configure(image=app.icon_dict[item["weather"][0]["icon"]])

    # 気温を表示
    app.wwt[count].configure(text="{0}°c".format(round(item["main"]["temp"])))

    # 降水量を表示
    rainfall = 0
    if "rain" in item and "3h" in item["rain"]:
        rainfall = item["rain"]["3h"]
    app.wwr[count].configure(text="{0}mm".format(math.ceil(rainfall)))

    # 表示カウンタを更新
    count += 1

    # 全て表示し終えたらループ終了
    if count >= len(app.wwl):
        # 地域情報を表示
        app.wp.configure(text="{0}, {1} (lat:{2}, lon:{3})".format(
            forecastData["city"]["country"],
            forecastData["city"]["name"],
            forecastData["city"]["coord"]["lat"],
            forecastData["city"]["coord"]["lon"]))

        # 60 秒間隔で繰り返す
        root.after(60000, update_weather)

OpenWeatherMap 無料版は 1 分間に 60 リクエスト(1 回/秒)までとなっているため、ゆっくり 60 秒毎の更新で許容範囲内となります。まあ、個人的な利用で 60 リクエスト以上になることは多分ないでしょう。

ソースコード

かろうじて、全部のソースコードを載せられるぐらいの量に納まっています。

clock.py
from tkinter import *
import tkinter.ttk as ttk
import os
import requests
import json
import math
from PIL import Image, ImageTk
from datetime import datetime, timedelta

# OpenWeatherMap の情報
KEY = "********************************"
ZIP = "101-0032,JP"
URL = "http://api.openweathermap.org/data/2.5/forecast?zip={0}&units=metric&lang=ja&APPID={1}"

# メインウィンドウ作成
root = Tk()

# メインウィンドウサイズ
root.geometry("1024x768")

# メインウィンドウタイトル
root.title("Clock")

# MainFrame クラス
class MainFrame(ttk.Frame):
    # コンストラクタ
    def __init__(self, master=None, **kwargs):
        # 親クラスのコンストラクタを呼び出す
        super().__init__(master, **kwargs)

        # create_widgets を呼び出す
        self.create_widgets()

    # ウィジェットを作成
    def create_widgets(self):
        # フレームを作成
        self.frame=Frame(self, bg="white", bd=0, height=460, relief="flat")

        # 時刻表示(配置は直接指定)
        self.wt=Label(self.frame, text="", bg="white", font=("", 160))
        self.wt.place(width=884, x=60, y=120)

        # 日付表示(配置は直接指定)
        self.wd=Label(self.frame, text="", bg="white", font=("", 60, "bold"))
        self.wd.place(width=744, x=160, y=60)

        # 地域表示(左寄せ)
        self.wp=Label(self.frame, text="", bg="white", fg="gray", font=("", 20, "bold"), anchor="w")
        self.wp.place(width=920, x=42, y=440)

        # フレームを配置
        self.frame.grid(row=0, column=0, columnspan=8, sticky="news")

        # このスクリプトの絶対パス
        self.scr_path = os.path.dirname(os.path.abspath(sys.argv[0]))

        # 天候アイコン(ディクショナリ)
        self.icon_dict={
            "01d":Image.open(self.scr_path + "/img/01d.png"), "01n":Image.open(self.scr_path + "/img/01n.png"),
            "02d":Image.open(self.scr_path + "/img/02d.png"), "02n":Image.open(self.scr_path + "/img/02n.png"),
            "03d":Image.open(self.scr_path + "/img/03.png"),  "03n":Image.open(self.scr_path + "/img/03.png"),
            "04d":Image.open(self.scr_path + "/img/04.png"),  "04n":Image.open(self.scr_path + "/img/04.png"),
            "09d":Image.open(self.scr_path + "/img/09.png"),  "09n":Image.open(self.scr_path + "/img/09.png"),
            "10d":Image.open(self.scr_path + "/img/10.png"),  "10n":Image.open(self.scr_path + "/img/10.png"),
            "11d":Image.open(self.scr_path + "/img/11.png"),  "11n":Image.open(self.scr_path + "/img/11.png"),
            "13d":Image.open(self.scr_path + "/img/13.png"),  "13n":Image.open(self.scr_path + "/img/13.png"),
            "50d":Image.open(self.scr_path + "/img/50.png"),  "50n":Image.open(self.scr_path + "/img/50.png")
        }

        # アイコンサイズを画面サイズにフィット(64x64)させる
        for key, value in self.icon_dict.items():
            self.icon_dict[key]=self.icon_dict[key].resize((64, 64), Image.ANTIALIAS)
            self.icon_dict[key]=ImageTk.PhotoImage(self.icon_dict[key])

        # 天気予報(時間帯)
        self.wwl=[
            Label(self, text="3",  bg="white", font=("", 30, "bold")),
            Label(self, text="6",  bg="white", font=("", 30, "bold")),
            Label(self, text="9",  bg="white", font=("", 30, "bold")),
            Label(self, text="12", bg="white", font=("", 30, "bold")),
            Label(self, text="15", bg="white", font=("", 30, "bold")),
            Label(self, text="18", bg="white", font=("", 30, "bold")),
            Label(self, text="21", bg="white", font=("", 30, "bold")),
            Label(self, text="24", bg="white", font=("", 30, "bold"))
        ]

        # 天気予報(時間帯)を配置
        for i in range(len(self.wwl)):
            self.wwl[i].grid(row=1, column=i, sticky="news")

        # 天気予報(天候)
        self.wwi=[
            Label(self, image=self.icon_dict["01d"], bg="white"),
            Label(self, image=self.icon_dict["01d"], bg="white"),
            Label(self, image=self.icon_dict["01d"], bg="white"),
            Label(self, image=self.icon_dict["01d"], bg="white"),
            Label(self, image=self.icon_dict["01d"], bg="white"),
            Label(self, image=self.icon_dict["01d"], bg="white"),
            Label(self, image=self.icon_dict["01d"], bg="white"),
            Label(self, image=self.icon_dict["01d"], bg="white")
        ]

        # 天気予報(天候)を配置
        for i in range(len(self.wwi)):
            self.wwi[i].grid(row=2, column=i, sticky="news")

        # 天気予報(気温)
        self.wwt=[
            Label(self, text="0°C", bg="white", font=("", 20)),
            Label(self, text="0°C", bg="white", font=("", 20)),
            Label(self, text="0°C", bg="white", font=("", 20)),
            Label(self, text="0°C", bg="white", font=("", 20)),
            Label(self, text="0°C", bg="white", font=("", 20)),
            Label(self, text="0°C", bg="white", font=("", 20)),
            Label(self, text="0°C", bg="white", font=("", 20)),
            Label(self, text="0°C", bg="white", font=("", 20))
        ]

        # 天気予報(気温)を配置
        for i in range(len(self.wwt)):
            self.wwt[i].grid(row=3, column=i, sticky="news")

        # 天気予報(降水量)
        self.wwr=[
            Label(self, text="0mm", bg="white", font=("", 20)),
            Label(self, text="0mm", bg="white", font=("", 20)),
            Label(self, text="0mm", bg="white", font=("", 20)),
            Label(self, text="0mm", bg="white", font=("", 20)),
            Label(self, text="0mm", bg="white", font=("", 20)),
            Label(self, text="0mm", bg="white", font=("", 20)),
            Label(self, text="0mm", bg="white", font=("", 20)),
            Label(self, text="0mm", bg="white", font=("", 20))
        ]

        # 天気予報(降水量)を配置
        for i in range(len(self.wwr)):
            self.wwr[i].grid(row=4, column=i, sticky="news")

        # レイアウト
        self.rowconfigure(0, weight=1)
        self.rowconfigure(1, weight=1)
        self.rowconfigure(2, weight=1)
        self.rowconfigure(3, weight=1)
        self.rowconfigure(4, weight=1)
        for i in range(len(self.wwl)):
            self.columnconfigure(i, weight=1)

# メインフレームを配置
app=MainFrame(root)
app.pack(side=TOP, expand=1, fill=BOTH)

# メインウィンドウを閉じる
def wm_close():
    root.destroy()

# 閉じるボタン作成
btn=Button(root, text=" X ", font=('', 16), relief=FLAT, command=wm_close)

# 画面がリサイズされたとき
def change_size(event):
    # ボタンの位置を右上に
    btn.place(x=root.winfo_width() - 60, y=14)

# 画面のリサイズをバインドする
root.bind('<Configure>', change_size)

# メインウィンドウの最大化
#root.attributes("-zoom", "1")
root.attributes("-fullscreen", "1")

# 常に最前面に表示
root.attributes("-topmost", True)

def update_time():
    # 現在日時を表示
    now=datetime.now()
    d="{0:0>4d}/{1:0>2d}/{2:0>2d} ({3}.)".format(now.year, now.month, now.day, now.strftime("%a"))
    t="{0:0>2d}:{1:0>2d}:{2:0>2d}".format(now.hour, now.minute, now.second)
    app.wd.configure(text=d)
    app.wt.configure(text=t)

    # 1秒間隔で繰り返す
    root.after(1000, update_time)

# 初回起動
update_time()

def update_weather():
    # 表示カウンタ
    count=0

    # URL を作成して OpenWeatherMap に問い合わせを行う
    url=URL.format(ZIP, KEY)
    response=requests.get(url)
    forecastData=json.loads(response.text)

    # 結果が得られない場合は即時終了
    if not ("list" in forecastData):
        print("error")
        return

    # デバッグ用
    print(forecastData)

    # 結果を 3 時間単位で取得
    for item in forecastData["list"]:
        # 時間帯を 24 時間表記で表示
        forecastDatetime = datetime.fromtimestamp(item["dt"])
        app.wwl[count].configure(text=forecastDatetime.hour)

        # 気候をアイコンで表示
        app.wwi[count].configure(image=app.icon_dict[item["weather"][0]["icon"]])

        # 気温を表示
        app.wwt[count].configure(text="{0}°c".format(round(item["main"]["temp"])))

        # 降水量を表示
        rainfall = 0
        if "rain" in item and "3h" in item["rain"]:
            rainfall = item["rain"]["3h"]
        app.wwr[count].configure(text="{0}mm".format(math.ceil(rainfall)))

        # 表示カウンタを更新
        count += 1

        # 全て表示し終えたらループ終了
        if count >= len(app.wwl):
            # 地域情報を表示
            app.wp.configure(text="{0}, {1} (lat:{2}, lon:{3})".format(
                forecastData["city"]["country"],
                forecastData["city"]["name"],
                forecastData["city"]["coord"]["lat"],
                forecastData["city"]["coord"]["lon"]))

            # 60 秒間隔で繰り返す
            root.after(60000, update_weather)

            return

# 初回起動
update_weather()

# コールバック関数を登録
root.after(1000,  update_time)
root.after(60000, update_weather)

# メインループ
root.mainloop()

完成

上から日付、時間、地域情報、予報時間帯、天候、気温、降水量です。
右上の × ボタンでプログラムを終了します。
IMG_20190318_135425.jpg

33
32
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
33
32