見出し画像

Strava/Withings Web API開発 - 開発編①(OpenWeather)

以下のリンクを参照して開発しました。
@Kazuya_Murakami氏がソースコードも公開してくれていて、殆どの箇所をほぼそのままの形で使わせて頂きました。感謝です。

(参考)Weather API(公式)


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

概要

コーディングの詳細は上記リンクやソースコードを見て頂くとして、要点を以下に整理します。

①メインウィンドウ/フレームを準備(
Tkinter
フレームは以下の3つのフレーム構成としました。
 - 日付/地域表示フレーム(frame1)
    - 天気予報(日次)表示フレーム(frame2)
    - 天気予報(3時間毎)表示フレーム(frame3)

<イメージ図>

フレーム構成

frame1は2行、frame2は4行(日付、天気、気温、降水量)×7列(月~日)、frame3は4行(時間、天気、気温、降水量)×8列(3時間毎×8項目)のレイアウトです。

また、天気は以下の通りコード化されているので、それに応じたアイコンを準備する必要があります。

OpenWeatherMapの天気コード
<日中>
01d  clear sky  快晴
02d  few clouds 晴れ
03d  scattered clouds くもり
04d  broken clouds くもり
09d  shower rain 小雨
10d  rain 雨
11d  thunderstorm 雷雨
13d  snow 雪
50d  mist 霧
<夜間>
01n  clear sky  快晴
02n  few clouds 晴れ
03n  scattered clouds くもり
04n  broken clouds くもり
09n  shower rain 小雨
10n  rain 雨
11n  thunderstorm 雷雨
13n  snow 雪
50n  mist 霧

アイコンは@Kazuya_Murakami氏同様に、 ICOOON MONOから頂いたものを編集して作りました。

天気アイコン


②各パーツの準備
各項目を表示するためのLabelを作成しフレームとの紐づけを行います。
- 日付・時刻表示用のLabelと地域表示用のLabel -> frame1 
- 4行(日付、天気、気温、降水量)×7列のLabel -> frame2
- 4行(時間、天気、気温、降水量)×8列のLabel -> frame3

それぞれのLabelに初期値をセットします。

③日次の天気予報データ取得(Requests/JSON)
リクエストを実行(One Call APIへResuests)し、取得したJSONデータを辞書型で格納します。JSONデータの構成は以下の通り。
   - currentのブロック
   - hourlyのブロック
   - dailyのブロック

<サンプル>

   }
 ],
 "daily": [
    {
     "dt": 1587952800,
     "sunrise": 1587930901,
     "sunset": 1587979463,
     "temp": {
       "day": 16.58,
       "min": 10.65,
       "max": 16.58,
       "night": 10.65,
       "eve": 14.76,
       "morn": 14.74
     },
     "feels_like": {
       "day": 13.42,
       "night": 6.31,
       "eve": 11.06,
       "morn": 11.5
     },
     "pressure": 1014,
     "humidity": 55,
     "dew_point": 7.7,
     "wind_speed": 3.68,
     "wind_deg": 93,
     "weather": [
       {
         "id": 502,
         "main": "Rain",
         "description": "強い雨",
         "icon": "10d"
       }
     ],
     "clouds": 88,
     "rain": 21.53,
     "uvi": 6.55
   },
   {
     "dt": 1588039200,
     "sunrise": 1588017234,
     "sunset": 1588065913,
            :

辞書型で格納したデータから、"daily"のブロックを特定して、その中の1件1件に対して以下の処理を行います。
 - "dt"(タイムスタンプ)をもとに月(mm)、日(dd)を取得
 - "weather" -> "icon"から天気コードを取得
 - "temp" -> "day"から気温データを取得
 - "rain"から降水量データを取得(雨が降らなければ出力されない)

④3時間毎の天気予報データ取得(Reuests/JSON)
リクエストを実行(5day/3hour Forecast APIへResuests)し、取得したJSONデータを辞書型で格納します。JSONデータの構成は以下の通り。
   - listのブロック
   - cityのブロック

<サンプル>

  "list": [
   {
     "dt": 1587643200,
     "main": {
       "temp": 12.93,
       "feels_like": 6.63,
       "temp_min": 12.93,
       "temp_max": 12.96,
       "pressure": 1008,
       "sea_level": 1008,
       "grnd_level": 1008,
       "humidity": 57,
       "temp_kf": -0.03
     },
     "weather": [
       {
         "id": 500,
         "main": "Rain",
         "description": "小雨",
         "icon": "10n"
       }
     ],
     "clouds": {
       "all": 52
     },
     "wind": {
       "speed": 7.29,
       "deg": 4
     },
     "rain": {
       "3h": 0.11
     },
     "sys": {
       "pod": "n"
     },
     "dt_txt": "2020-05-22 12:00:00"
   },
   {
     "dt": 1587654000,

辞書型で格納したデータから、"list"のブロックを特定して、その中の1件1件に対して以下の処理を行います。
 - "dt"(タイムスタンプ)をもとに時間(hh)を取得
 - "weather" -> "icon"から天気コードを取得
 - "main" -> "temp"から気温データを取得
 - "rain"から降水量データを取得(雨が降らなければ出力されない)

⑤各データを予め用意していたLabelにセットし画面表示
最終的に以下のような表示になりました。

天気予報イメージ


ソースコード

################################ 
###     OpenWeatherMap     ##### 
################################ 
###
## 日付・時刻						openWeatherDt
## 地域							openWeatherPlace
## 天候アイコン(ディクショナリ)	    openWeatherIconDict
## 天気予報(日付):日次			openWeatherDailyDate
## 天気予報(天候):日次			openWeatherDailyForecast
## 天気予報(気温):日次			openWeatherDailyTemp
## 天気予報(降水量):日次			openWeatherDailyRain
## 天気予報(時間帯):3時間毎		openWeather3HourHour
## 天気予報(天候):3時間毎		    openWeather3HourForecast
## 天気予報(気温):3時間毎		    openWeather3HourTemp
## 天気予報(降水量):3時間毎		openWeather3HourRain
## 表示日付(例:2020/5/21 (Thr.))displayDate
## 表示時刻(例:15:35:56)			displayTime
## One Call API(日次)URL		openWeatherDailyUrl
## One Call APIパラメーター		openWeatherDailyUrlParams
## One Call API取得データ			forecastDailyData
## One Call API取得データ処理カウンタ	countDailyData
## Forecast API(3時間毎)URL		openWeather3HourUrl
## Forecast API パラメーター		openWeather3HourUrlParams
## Forecast API取得データ			forecast3HourData 
## Forecast API取得データ処理カウンタ	count3HourData
###

## 各種パッケージのインポート
from tkinter import *  
import os
import requests
import json
import math
from PIL import Image, ImageTk
import datetime

## OpenWeatherMapへのWeb APIアクセス時のパラメーター(設定値)
TZ = "Asia/Tokyo"
LAT = "35.41"
LON = "139.45"
ZIP = "135-0023,JP"
KEY = "abcdefghijklmnopqrstuvwxyz12345...."

## OpenWeatherMapへのWeb APIアクセスURL、パラメーター
openWeatherDailyUrl = "https://api.openweathermap.org/data/2.5/onecall"
openWeatherDailyUrlParams = {"lat": LAT,"lon": LON, "units":"metric","APPID": KEY}
openWeather3HourUrl = "http://api.openweathermap.org/data/2.5/forecast"
openWeather3HourUrlParams = {"zip": ZIP,"units":"metric","lang":"ja","APPID": KEY}

##################################    メイン処理    ##################################

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

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

## フレームを作成
frame1=Frame(root, bg="white")    ##日付/地域表示フレーム
frame2=Frame(root, bg="white")    ##天気予報(日次)表示フレーム
frame3=Frame(root, bg="white")    ##天気予報(3時間毎)表示フレーム

########################    日付/地域表示フレーム(frame1)    ########################

## 日付・時刻表示(frame1の1行目に表示)
openWeatherDt=Label(root, text="", bg="white", font=("", 30, "bold"))
openWeatherDt.grid(in_=frame1,row=0, column=0, sticky="news")

## 地域表示(frame1の2行目に表示)
openWeatherPlace=Label(root, text="", bg="white", fg="gray", font=("", 10, "bold"))
openWeatherPlace.grid(in_=frame1,row=1, column=0, sticky="news")


########################    天気予報(日次)表示フレーム(frame2)    ########################

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

## 天候アイコン(ディクショナリ)
openWeatherIconDict={
   "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 openWeatherIconDict.items():
   openWeatherIconDict[key]=openWeatherIconDict[key].resize((64, 64), Image.ANTIALIAS)
   openWeatherIconDict[key]=ImageTk.PhotoImage(openWeatherIconDict[key])

## 天気予報(日付)を配置(frame2の1行目に表示)
openWeatherDailyDate=[
   Label(root, text="X", bg="white", font=("", 20, "bold")),
   Label(root, text="X", bg="white", font=("", 20, "bold")),
   Label(root, text="X", bg="white", font=("", 20, "bold")),
   Label(root, text="X", bg="white", font=("", 20, "bold")),
   Label(root, text="X", bg="white", font=("", 20, "bold")),
   Label(root, text="X", bg="white", font=("", 20, "bold")),
   Label(root, text="X", bg="white", font=("", 20, "bold"))
]
for i in range(len(openWeatherDailyDate)):
   openWeatherDailyDate[i].grid(in_=frame2,row=0, column=i, sticky="new")

## 天気予報(天候)を配置(frame2の2行目に表示)
openWeatherDailyForecast=[
   Label(root, image=openWeatherIconDict["01d"], bg="white"),
   Label(root, image=openWeatherIconDict["01d"], bg="white"),
   Label(root, image=openWeatherIconDict["01d"], bg="white"),
   Label(root, image=openWeatherIconDict["01d"], bg="white"),
   Label(root, image=openWeatherIconDict["01d"], bg="white"),
   Label(root, image=openWeatherIconDict["01d"], bg="white"),
   Label(root, image=openWeatherIconDict["01d"], bg="white")
]
for i in range(len(openWeatherDailyForecast)):
   openWeatherDailyForecast[i].grid(in_=frame2,row=1, column=i, sticky="new")


## 天気予報(気温)を配置(frame2の3行目に表示)
openWeatherDailyTemp=[
   Label(root, text="X°C", bg="white", font=("", 16, "bold")),
   Label(root, text="X°C", bg="white", font=("", 16, "bold")),
   Label(root, text="X°C", bg="white", font=("", 16, "bold")),
   Label(root, text="X°C", bg="white", font=("", 16, "bold")),
   Label(root, text="X°C", bg="white", font=("", 16, "bold")),
   Label(root, text="X°C", bg="white", font=("", 16, "bold")),
   Label(root, text="X°C", bg="white", font=("", 16, "bold"))
]
for i in range(len(openWeatherDailyTemp)):
   openWeatherDailyTemp[i].grid(in_=frame2,row=2, column=i, sticky="new")

## 天気予報(降水量)(frame2の4行目に表示)
openWeatherDailyRain=[
   Label(root, text="Xmm", bg="white", font=("", 16, "bold")),
   Label(root, text="Xmm", bg="white", font=("", 16, "bold")),
   Label(root, text="Xmm", bg="white", font=("", 16, "bold")),
   Label(root, text="Xmm", bg="white", font=("", 16, "bold")),
   Label(root, text="Xmm", bg="white", font=("", 16, "bold")),
   Label(root, text="Xmm", bg="white", font=("", 16, "bold")),
   Label(root, text="Xmm", bg="white", font=("", 16, "bold"))
]
for i in range(len(openWeatherDailyRain)):
   openWeatherDailyRain[i].grid(in_=frame2,row=3, column=i, sticky="new")


########################    天気予報(3時間毎)表示フレーム(frame3)    ########################

## 天気予報(時間)を配置(frame3の1行目に表示)
openWeather3HourHour=[
  Label(root, text="X", bg="white", font=("", 20, "bold")),
  Label(root, text="X", bg="white", font=("", 20, "bold")),
  Label(root, text="X", bg="white", font=("", 20, "bold")),
  Label(root, text="X", bg="white", font=("", 20, "bold")),
  Label(root, text="X", bg="white", font=("", 20, "bold")),
  Label(root, text="X", bg="white", font=("", 20, "bold")),
  Label(root, text="X", bg="white", font=("", 20, "bold")),
  Label(root, text="X", bg="white", font=("", 20, "bold"))
]
for i in range(len(openWeather3HourHour)):
   openWeather3HourHour[i].grid(in_=frame3,row=0, column=i, sticky="news")

## 天気予報(天候)を配置(frame3の2行目に表示)
openWeather3HourForecast=[
   Label(root, image=openWeatherIconDict["01d"], bg="white"),
   Label(root, image=openWeatherIconDict["01d"], bg="white"),
   Label(root, image=openWeatherIconDict["01d"], bg="white"),
   Label(root, image=openWeatherIconDict["01d"], bg="white"),
   Label(root, image=openWeatherIconDict["01d"], bg="white"),
   Label(root, image=openWeatherIconDict["01d"], bg="white"),
   Label(root, image=openWeatherIconDict["01d"], bg="white"),
   Label(root, image=openWeatherIconDict["01d"], bg="white")
]
for i in range(len(openWeather3HourForecast)):
   openWeather3HourForecast[i].grid(in_=frame3,row=1, column=i, sticky="news")
  
## 天気予報(気温)を配置(frame3の3行目に表示)
openWeather3HourTemp=[
   Label(root, text="X°C", bg="white", font=("", 16, "bold")),
   Label(root, text="X°C", bg="white", font=("", 16, "bold")),
   Label(root, text="X°C", bg="white", font=("", 16, "bold")),
   Label(root, text="X°C", bg="white", font=("", 16, "bold")),
   Label(root, text="X°C", bg="white", font=("", 16, "bold")),
   Label(root, text="X°C", bg="white", font=("", 16, "bold")),
   Label(root, text="X°C", bg="white", font=("", 16, "bold")),
   Label(root, text="X°C", bg="white", font=("", 16, "bold"))
]
for i in range(len(openWeather3HourTemp)):
   openWeather3HourTemp[i].grid(in_=frame3,row=2, column=i, sticky="news")

## 天気予報(降水量)を配置(frame3の4行目に表示)
openWeather3HourRain=[
   Label(root, text="Xmm", bg="white", font=("", 16, "bold")),
   Label(root, text="Xmm", bg="white", font=("", 16, "bold")),
   Label(root, text="Xmm", bg="white", font=("", 16, "bold")),
   Label(root, text="Xmm", bg="white", font=("", 16, "bold")),
   Label(root, text="Xmm", bg="white", font=("", 16, "bold")),
   Label(root, text="Xmm", bg="white", font=("", 16, "bold")),
   Label(root, text="Xmm", bg="white", font=("", 16, "bold")),
   Label(root, text="Xmm", bg="white", font=("", 16, "bold"))
]
for i in range(len(openWeather3HourRain)):
   openWeather3HourRain[i].grid(in_=frame3,row=3, column=i, sticky="news")

## ジオメトリマネージャーで各項目を表示
frame1.pack(fill='both',expand=True)
frame2.pack(fill='both',expand=True)
frame3.pack(fill='both',expand=True)

## レイアウト調整(横幅を画面に合わせる)
frame1.columnconfigure(0, weight=1)  ##frame1は1項目なので引数は0

for i in range(len(openWeatherDailyDate)):   ##frame2は7項目なので0~6を順番に実行
   frame2.columnconfigure(i, weight=1)

for i in range(len(openWeather3HourHour)):   ##frame3は8項目なので0~7を順番に実行
   frame3.columnconfigure(i, weight=1)

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

## Closeボタン(X)
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("-fullscreen", "1")
root.attributes("-topmost", True)

########################    日時データ取得/更新    ########################

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

   # 地域情報を表示
   openWeatherPlace.configure(text=TZ + " ( lat: " + LAT + " , lon: " + LON + " ) ")

   # 1秒間隔でUpdate
   root.after(1000, updateTime)

########################    天気予報(日次)データ取得/更新    ########################

def updateDailyWeather():
   # 表示カウンタ
   countDailyData=0

   # OpenWeatherMap に問い合わせを行う
   response=requests.get(openWeatherDailyUrl,params=openWeatherDailyUrlParams)
   forecastDailyData=json.loads(response.text)

   # 結果が得られない場合は即時終了
   if not ("daily" in forecastDailyData):
       print("error")
       return
   
   # 結果を 1日単位で取得
   for item in forecastDailyData["daily"]:
       # 日付を表示(mm/dd)
       openWeatherDailyDate[countDailyData].configure(text=str(datetime.datetime.fromtimestamp(item["dt"]).month) + "/" + str(datetime.datetime.fromtimestamp(item["dt"]).day))

       # 気候をアイコンで表示
       openWeatherDailyForecast[countDailyData].configure(image=openWeatherIconDict[item["weather"][0]["icon"]])

       # 気温を表示
       openWeatherDailyTemp[countDailyData].configure(text="{0}°c".format(round(item["temp"]["day"])))

       # 降水量を表示
       rainfall = 0
       if "rain" in item:
           rainfall = item["rain"]
       openWeatherDailyRain[countDailyData].configure(text="{0}mm".format(math.ceil(rainfall)))

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

       # 全て表示し終えたらループ終了
       if countDailyData >= len(openWeatherDailyDate):

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

           return


########################    天気予報(3時間毎)データ取得/更新    ########################

def update3hourWeather():
   
   # 表示カウンタ
   count3HourData=0

   # OpenWeatherMap に問い合わせを行う
   response=requests.get(openWeather3HourUrl,params=openWeather3HourUrlParams)
   forecast3HourData=json.loads(response.text)

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

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

       # 気候をアイコンで表示
       openWeather3HourForecast[count3HourData].configure(image=openWeatherIconDict[item["weather"][0]["icon"]])

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

       # 降水量を表示
       rainfall = 0
       if "rain" in item and "3h" in item["rain"]:
       	rainfall = item["rain"]["3h"]
       openWeather3HourRain[count3HourData].configure(text="{0}mm".format(math.ceil(rainfall)))
       
       # 表示カウンタを更新
       count3HourData += 1

       # 全て表示し終えたらループ終了
       if count3HourData >= len(openWeather3HourHour):

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

           return


# 初回起動
updateTime()
updateDailyWeather()
update3hourWeather()

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

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


<関連記事>
Strava/Withings Web API開発(天気と走行距離と体重をダッシュボードで表示してみた)
https://note.com/sanoatsu/n/nbe00ce35d3b1
Strava/Withings Web API開発 - 全体概要
https://note.com/sanoatsu/n/ncc074b9ce5f7
Strava/Withings Web API開発 - 準備編①
https://note.com/sanoatsu/n/n0104f85631cf
Strava/Withings Web API開発 - 準備編②
https://note.com/sanoatsu/n/n4d05a6f1cb2d
Strava/Withings Web API開発 - 準備編③
https://note.com/sanoatsu/n/nb60129601d54

Strava/Withings Web API開発 - Pythonリファレンス
Web API認証
https://note.com/sanoatsu/n/n98742c974b17
GUIレイアウト作成:Tkinter
https://note.com/sanoatsu/n/ncb830ae933dd/
日付・時刻処理:datetime
https://note.com/sanoatsu/n/n050ca16df6c7/
HTTPでのデータ取得:Requests
https://note.com/sanoatsu/n/n75e81bc2896a
データ格納:JSONと配列、辞書
https://note.com/sanoatsu/n/na76312571869/


よろしければサポートお願いします。