前回の記事:【Python】raspberry pi 置き時計に天気予報を追加した件 の続きです。
天気予報の次はセンサーを使って、実際の気温と温度を測定したいと思いますが、
その前に増えてきた機能の整理です。
前回コメントでアドバイスをいただきましたので、勉強がてら整理してみました。見た目も少し改修しました。
勉強になったこと
- 機能(プログラムソース)の分離
- 設定ファイル
- フォントのインストール
GitHub
画面配置
解説
clock.py
本家プログラムファイルでしたが、日時を表示する時計機能だけになりました。
単独での起動 python clock.py
が可能です。
from tkinter import Tk, Canvas, Label
from datetime import datetime
# 卓上時計クラス
class Clock(Canvas):
# コンストラクタ
def __init__(self, master):
# 親クラスのコンストラクタ
super().__init__(master, bg="white")
# 時刻表示
self.wt1=Label(self, bg="white", font=("DSEG7 Classic", 140, "bold"))
self.wt1.grid(row=0, column=0, ipady=50, sticky="news")
self.wt2=Label(self, bg="white", font=("DSEG7 Classic", 70, "bold"))
self.wt2.grid(row=0, column=1, ipadx=5, sticky="news")
# 日付表示
self.wd=Label(self, bg="white", font=("", 50, "bold"))
self.wd.grid(row=1, column=0, columnspan=2, sticky="news")
# 表示を更新
def update(self):
# 現在日時を表示
now=datetime.now()
self.wd.configure(text=now.strftime("%Y/%m/%d (%a.)"))
self.wt1.configure(text=now.strftime("%H:%M"))
self.wt2.configure(text=now.strftime("%S"))
# 1秒後に再表示
self.master.after(1000, self.update)
# 単独処理の場合
def main():
# メインウィンドウ作成
root=Tk()
# メインウィンドウタイトル
root.title("Clock")
# メインウィンドウサイズ
root.geometry("1024x768")
# メインウィンドウの最大化
root.attributes("-zoom", "1")
# 常に最前面に表示
root.attributes("-topmost", True)
# メインウィンドウの背景色
root.configure(bg="white")
# Clock クラスのインスタンスを生成
clock=Clock(root)
# 画面に配置
clock.pack(expand=1)
# 時計表示の更新を開始(update メソッド呼び出し)
clock.update()
# メインループ
root.mainloop()
# import clock による呼び出しでなければ単独処理 main() を実行
if __name__ == "__main__":
main()
時分と秒を分けて配置、時間のフォントをデジタル時計っぽいものに変えました。
完成形は一番下の写真をご覧ください。
フォントのインストールは割と簡単。
ダウンロードしたフォントファイルを /usr/share/fonts
あたりに配置するだけです。
今回は TrueType フォントなので truetype
ディレクトリ配下に置きました。
fc-cache
で反映、fc-list
を grep 検索して目的のフォントが認識されていれば OK です。
ちなみに、アンインストールは rm -R
でディレクトリごと削除して fc-cache
で再度反映するだけ。
$ scp -r ~/Downloads/fonts-DSEG_v045 pi@192.168.X.X:/home/pi/python
pi@192.168.X.X's password:
$ ssh pi@192.168.X.X
pi@192.168.X.X's password:
$ sudo cp -r ~/python/fonts-DSEG_v045 /usr/share/fonts/truetype/fonts-DSEG_v045
$ fc-cache
$ fc-list | grep "DSEG7 Classic:style=Bold"
/usr/share/fonts/truetype/fonts-DSEG_v045/DSEG7-Classic/DSEG7Classic-BoldItalic.ttf: DSEG7 Classic:style=Bold Italic
/usr/share/fonts/truetype/fonts-DSEG_v045/DSEG7-Classic/DSEG7Classic-Bold.woff:DSEG7 Classic:style=Bold
/usr/share/fonts/truetype/fonts-DSEG_v045/DSEG7-Classic/DSEG7Classic-BoldItalic.woff: DSEG7 Classic:style=Bold Italic
/usr/share/fonts/truetype/fonts-DSEG_v045/DSEG7-Classic/DSEG7Classic-Bold.ttf: DSEG7 Classic:style=Bold
↓アンインストール方法
$ sudo rm -R /usr/share/fonts/truetype/fonts-DSEG_v045
$ fc-cache
$ fc-list | grep "DSEG14 Classic:style=Bold"
フォントは以下のサイトから頂戴しました。
参考リンク:7セグ・14セグフォント 「DSEG」
weather.py
天気予報機能を集約しています。
こちらも単独での起動 python weather.py
が可能です。
OpenWeatherMap の情報をソースコード直書きから設定ファイルに外出し、あと風向きと強さを新たに追加しました。
なお、風向きの表示方法についてはこちらのサイトが参考になりました。
参考リンク:OpenWeatherMapをショートコードにして各地の天気を表示 | ゆうそうとITブログ
★コメントをいただきました
しらかみゅさんから weather.py のフレーム化についてコメントをいただきました。とても洗練されてますね。
下記のコードは初心者向けということでご容赦ください。
from tkinter import Tk, Frame, Label
from datetime import datetime
from PIL import Image, ImageTk
import configparser
import json
import math
import os
import requests
import sys
# このスクリプトの絶対パス
scr_path = os.path.dirname(os.path.abspath(sys.argv[0]))
# 設定ファイルから取得(Python 3.X 用)
inifile=configparser.ConfigParser()
inifile.read(scr_path + "/weather.ini", "UTF-8")
# OpenWeatherMap の情報
KEY = inifile.get("settings", "key")
ZIP = inifile.get("settings", "zip")
URL = inifile.get("settings", "url")
# 天気予報クラス
class Weather(Frame):
# コンストラクタ
def __init__(self, master):
# 親クラスのコンストラクタ
super().__init__(master, bd=0, bg="white", relief="flat")
# 地域表示(左寄せ)
self.wp=Label(self, bg="white", fg="gray", font=("", 20, "bold"), anchor="w")
self.wp.grid(row=0, column=0, columnspan=8, padx=20, sticky="news")
# 天候アイコン(ディクショナリー)
self.icon_dict={
"01d":Image.open(scr_path + "/img/01d.png"), "01n":Image.open(scr_path + "/img/01n.png"),
"02d":Image.open(scr_path + "/img/02d.png"), "02n":Image.open(scr_path + "/img/02n.png"),
"03d":Image.open(scr_path + "/img/03.png"), "03n":Image.open(scr_path + "/img/03.png"),
"04d":Image.open(scr_path + "/img/04.png"), "04n":Image.open(scr_path + "/img/04.png"),
"09d":Image.open(scr_path + "/img/09.png"), "09n":Image.open(scr_path + "/img/09.png"),
"10d":Image.open(scr_path + "/img/10.png"), "10n":Image.open(scr_path + "/img/10.png"),
"11d":Image.open(scr_path + "/img/11.png"), "11n":Image.open(scr_path + "/img/11.png"),
"13d":Image.open(scr_path + "/img/13.png"), "13n":Image.open(scr_path + "/img/13.png"),
"50d":Image.open(scr_path + "/img/50.png"), "50n":Image.open(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="0", bg="white", font=("", 30, "bold")),
Label(self, text="0", bg="white", font=("", 30, "bold")),
Label(self, text="0", bg="white", font=("", 30, "bold")),
Label(self, text="0", bg="white", font=("", 30, "bold")),
Label(self, text="0", bg="white", font=("", 30, "bold")),
Label(self, text="0", bg="white", font=("", 30, "bold")),
Label(self, text="0", bg="white", font=("", 30, "bold")),
Label(self, text="0", bg="white", font=("", 30, "bold"))
]
# 天気予報(時間帯)を配置
for i in range(len(self.wwl)):
self.wwl[i].grid(row=1, column=i, pady=5, 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, pady=5, 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, pady=5, 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, pady=5, sticky="news")
# 天気予報(風向き)
self.www=[
Label(self, text="0", bg="white", font=("", 20)),
Label(self, text="0", bg="white", font=("", 20)),
Label(self, text="0", bg="white", font=("", 20)),
Label(self, text="0", bg="white", font=("", 20)),
Label(self, text="0", bg="white", font=("", 20)),
Label(self, text="0", bg="white", font=("", 20)),
Label(self, text="0", bg="white", font=("", 20)),
Label(self, text="0", bg="white", font=("", 20))
]
# 天気予報(風向き)を配置
for i in range(len(self.www)):
self.www[i].grid(row=5, column=i, pady=5, 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)
self.rowconfigure(5, weight=1)
for i in range(len(self.wwl)):
self.columnconfigure(i, weight=1)
# 表示を更新
def update(self):
# 表示カウンタ
count=0
# URL を作成して OpenWeatherMap に問い合わせを行う
url=URL.format(ZIP, KEY)
response=requests.get(url)
forecastData=json.loads(response.text)
# 結果が得られない場合はエラー終了
if not ("list" in forecastData):
self.wp.configure(text="OpenWeatherMap request error!", fg="red")
return
# デバッグ用
print(forecastData)
# 結果を 3 時間単位で取得
for item in forecastData["list"]:
# 時間帯を 24 時間表記で表示
forecastDatetime = datetime.fromtimestamp(item["dt"])
self.wwl[count].configure(text=forecastDatetime.hour)
# 気候をアイコンで表示
self.wwi[count].configure(image=self.icon_dict[item["weather"][0]["icon"]])
# 気温を表示
self.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"]
self.wwr[count].configure(text="{0}mm".format(math.ceil(rainfall)))
# 風向きを表示
windspd=item["wind"]["speed"]
winddeg=item["wind"]["deg"]
# 風の方角を割り出す
if winddeg < 22.5:
# 北旋回
winddeg += 360
if winddeg > 337.5 and winddeg < 382.5:
# 北の風
self.www[count].configure(text="N, {0}m".format(math.ceil(windspd)))
elif winddeg > 22.5 and winddeg < 67.5:
# 北東の風
self.www[count].configure(text="NE, {0}m".format(math.ceil(windspd)))
elif winddeg > 67.5 and winddeg < 112.5:
# 東の風
self.www[count].configure(text="E, {0}m".format(math.ceil(windspd)))
elif winddeg > 112.5 and winddeg < 157.5:
# 南東の風
self.www[count].configure(text="SE, {0}m".format(math.ceil(windspd)))
elif winddeg > 157.5 and winddeg < 202.5:
# 南の風
self.www[count].configure(text="S, {0}m".format(math.ceil(windspd)))
elif winddeg > 202.5 and winddeg < 247.5:
# 南西の風
self.www[count].configure(text="SW, {0}m".format(math.ceil(windspd)))
elif winddeg > 247.5 and winddeg < 292.5:
# 西の風
self.www[count].configure(text="W, {0}m".format(math.ceil(windspd)))
elif winddeg > 292.5 and winddeg < 337.5:
# 北西の風
self.www[count].configure(text="NW, {0}m".format(math.ceil(windspd)))
# 表示カウンタを更新
count += 1
# 全て表示し終えたらループ終了
if count >= len(self.wwl):
# 地域情報を表示
self.wp.configure(text="{0}, {1} (lat:{2}, lon:{3})".format(
forecastData["city"]["country"],
forecastData["city"]["name"],
forecastData["city"]["coord"]["lat"],
forecastData["city"]["coord"]["lon"]), fg="gray")
# 60秒後に再表示
self.master.after(60000, self.update)
return
# 単独処理の場合
def main():
# メインウィンドウ作成
root=Tk()
# メインウィンドウタイトル
root.title("Weather")
# メインウィンドウサイズ
root.geometry("1024x768")
# メインウィンドウの最大化
root.attributes("-zoom", "1")
# 常に最前面に表示
root.attributes("-topmost", True)
# メインウィンドウの背景色
root.configure(bg="white")
# Weather クラスのインスタンスを生成
weather=Weather(root)
# 画面中央に配置
weather.pack(expand=1, fill="x")
# 天気予報の更新を開始(update メソッド呼び出し)
weather.update()
# メインループ
root.mainloop()
# import weather による呼び出しでなければ単独処理 main() を実行
if __name__ == "__main__":
main()
で、設定ファイルはこちら。
これでいちいちソースコードを書き換えずに済みます。
(設定ファイルがないと実行時エラーになります)
[settings]
key = ********************************
zip = 101-0032,JP
url = http://api.openweathermap.org/data/2.5/forecast?zip={0}&units=metric&lang=ja&APPID={1}
main.py
それぞれの機能を実行して、結果を画面に表示するメインフレームです。
時計、天気予報、このあと実装する予定のセンサー情報を表示します。
from tkinter import *
import tkinter.ttk as ttk
from clock import Clock
from sensor import Sensor
from weather import Weather
# MainFrame クラス
class MainFrame(ttk.Frame):
# コンストラクタ
def __init__(self, master=None, *args, **kwargs):
# 親クラスのコンストラクタ
super().__init__(args, **kwargs)
# Clock クラスのインスタンスを生成して配置
self.clock=Clock(self)
self.clock.grid(row=0, column=0, sticky="news")
# Sensor クラスのインスタンスを生成して配置
self.sensor=Sensor(self)
self.sensor.grid(row=0, column=1, sticky="news")
# Weather クラスのインスタンスを生成して配置
self.weather=Weather(self)
self.weather.grid(row=1, column=0, columnspan=2, sticky="news")
# レイアウト
self.columnconfigure(1, weight=1)
def update(self):
# 時計表示の更新を開始(Clock.update メソッド呼び出し)
self.clock.update()
# センサー表示の更新を開始(Sensor.update メソッド呼び出し)
self.sensor.update()
# 天気予報の更新を開始(Weather.update メソッド呼び出し)
self.weather.update()
# メインウィンドウ作成
root=Tk()
# メインウィンドウタイトル
root.title("Main")
# メインウィンドウサイズ
root.geometry("1024x768")
# メインウィンドウの最大化
#root.attributes("-zoom", "1")
root.attributes("-fullscreen", "1")
# 常に最前面に表示
root.attributes("-topmost", True)
# メインウィンドウの背景色
root.configure(bg="white")
# メインフレームを配置
app=MainFrame(root)
# 画面に配置
app.pack(fill="both")
# 閉じるボタン作成
close=Button(root, text=" X ", font=('', 16, "bold"), relief=FLAT, command=root.destroy)
# 画面がリサイズされたとき、ボタンの位置を右上に移動
def change_size(event):
close.place(x=root.winfo_width() - 60, y=14)
# 画面のリサイズをバインドする
root.bind('<Configure>', change_size)
# MainFrame.update メソッドを呼び出す
app.update()
# メインループ
root.mainloop()
sensor.py
温度、湿度、あとついでに大気圧センサーがあったので組み込んでみる予定です。
とりあえずラベル表示だけしてみました。
こちらも単独での起動 python sensor.py
が可能です。
from tkinter import Tk, Frame, Label
# センサー表示クラス
class Sensor(Frame):
# コンストラクタ
def __init__(self, master):
# 親クラスのコンストラクタ
super().__init__(master, bg="white")
# スペーサ(センサー表示上部の間隔調整)
self.wsp=Label(self, bg="white")
self.wsp.grid(row=0, column=0, ipady=84, sticky="news")
# 温度表示
self.wst1=Label(self, text="気温:", bg="white", font=("", 20, "bold"))
self.wst1.grid(row=1, column=0, pady=5, sticky="news")
self.wst2=Label(self, text="0°c", bg="lightblue", font=("", 30, "bold"))
self.wst2.grid(row=1, column=1, ipadx=5, pady=5, sticky="news")
# 湿度表示
self.wsh1=Label(self, text="湿度:", bg="white", font=("", 20, "bold"))
self.wsh1.grid(row=2, column=0, pady=5, sticky="news")
self.wsh2=Label(self, text="0%", bg="silver", font=("", 30, "bold"))
self.wsh2.grid(row=2, column=1, ipadx=5, pady=5, sticky="news")
# 気圧表示
self.wsp1=Label(self, text="気圧:1013", bg="white", font=("", 20, "bold"))
self.wsp1.grid(row=3, column=0, columnspan=2, pady=5, sticky="news")
# 表示を更新
def update(self):
# 1秒後に再表示
self.master.after(1000, self.update)
# 単独処理の場合
def main():
# メインウィンドウ作成
root=Tk()
# メインウィンドウタイトル
root.title("Sensor")
# メインウィンドウサイズ
root.geometry("1024x768")
# メインウィンドウの最大化
root.attributes("-zoom", "1")
# 常に最前面に表示
root.attributes("-topmost", True)
# メインウィンドウの背景色
root.configure(bg="white")
# Sensor クラスのインスタンスを生成
sensor=Sensor(root)
# 画面に配置
sensor.pack(expand=1, fill="y")
# センサー表示の更新を開始(update メソッド呼び出し)
sensor.update()
# メインループ
root.mainloop()
# import sensor による呼び出しでなければ単独処理 main() を実行
if __name__ == "__main__":
main()