はじめに
平日は基本的に、毎日ランニングをしているのですが、当然ながら雨の日や、休日に走る気はありません。
そこで、次の日走るかどうかの判断を自動化し、GoogleCalendarに予定を追加するプログラムをPythonで書いてみました。
環境
- macOS, Linux
- python 3.8.1
定期実行するのに crontab
を使っているため、macOSかLinuxを想定していますが、WindowsではAWSなどを使えば、同じようなことができるかもしれません。
Pythonの設定
まず、Pythonを設定します。個人的には、環境を汚すのが好きではないので、プロジェクト毎に virtualenv
を作っています。本題とはずれるので、詳しくはこちらを参考にしてください。
$ pyenv virtualenv 3.8.1 develop_3.8.1
$ pyenv activate develop_3.8.1
$ pip install --upgrade google-api-python-client google-auth-httplib2 google-auth-oauthlib
明日の天気を判定する
天気予報の取得には、OpenWeatherMapを使用しました。
使い方は至ってシンプルで、アカウントを作成したあとの「API keys」というタブからAPIキーを取得し、OPENWEATHER_API_KEY
にセットします。
ちなみに、今回は場所の指定に郵便番号を使いましたが、都市名や、緯度経度でも指定することができるようです。
import requests
BASE_URL = 'http://api.openweathermap.org/data/2.5/forecast'
OPENWEATHER_API_KEY = 'Your OpenWeather Api key'
ZIP_CODE = 'Zip code of your area'
url = '{}?units=metric&zip={zip},JP&APPID={key}'.format(BASE_URL, zip=ZIP_CODE, key=OPENWEATHER_API_KEY)
res = requests.get(url).json()
すると、このような結果が返ってきます。
{
"cod": "200",
"message": 0,
"cnt": 40,
"list": [
{
"dt": 150000000,
"main": {
"temp": 13.03,
"feels_like": 9.94,
"temp_min": 12.61,
"temp_max": 13.03,
"pressure": 1016,
"sea_level": 1016,
"grnd_level": 1010,
"humidity": 40,
"temp_kf": 0.42
},
"weather": [
{
"id": 803,
"main": "Clouds",
"description": "broken clouds",
"icon": "04d"
}
],
"clouds": {
"all": 60
},
"wind": {
"speed": 1.52,
"deg": 150
},
"sys": {
"pod": "d"
},
"dt_txt": "2020-02-29 06:00:00"
},
...
結果は、5日後までの3時間毎の結果が返ってきています。
list
の配列には、 dt_txt
の日時の天気予報が入っており、
- temp: 温度
- feels_like: 体感温度
- temp_min: 最低気温
- temp_max: 最高気温
- pressure: 大気圧
- sea_level: 海面の大気圧
- grnd_level: 地上の大気圧
- humidity: 湿度
などが取れるようです。情報量が多く、素晴らしいですね。しかし、今回は雨かどうか知りたいだけなので、 weather
の main
に着目したいと思います。
import datetime
tommorow = datetime.datetime.now() + datetime.timedelta(days=1)
tommorow_str = tommorow.strftime('%Y-%m-%d')
def is_rain(tommorow_str):
tommorow_morning_dt = [tommorow_str + ' 06:00:00', tommorow_str + ' 09:00:00']
tommorow_morning_weather = []
weather_preds = res['list']
for pred in weather_preds:
if pred['dt_txt'] in tommorow_morning_dt:
for weather in pred['weather']:
tommorow_morning_weather.append(weather['main'])
return 'Rain' in tommorow_morning_weather
まず、datetime
を用いて、明日の日付を計算します。
そして、ランニングするのは早朝なので、明日の午前6時ごろと、午前9時ごろの日付を文字列として tommorow_morning_dt
に入れておきます。そして先ほどの結果と比較し、 dt_txt
が目的の日付であれば、 tommorow_morning_weather
に追加しています。
これで明日のランニングをする時間帯の天気を取得することができました。しかし、この実装は正直微妙だと思っていて、日付を文字列で比較するのはナンセンスな気がするので、いい方法があったら教えてください。
休日かどうかを判断する
基本的に、平日以外は走る気がないので、休日は除外したいです。単に休日を除外するだけであれば話は簡単なのですが、祝日についても考慮しなければならないので、こちらを参考に、 jpholiday
というパッケージを使いました。先ほど作った雨かどうかを判定する is_rain()
も使用することで、ランニングをするときの条件を表すことができます。
ちなみに、 datetime
の weekday()
という関数は、月〜金が 0~4 に対応しており、土日は 5,6 に対応しています。
import jpholiday
if tommorow.weekday() < 5 and not jpholiday.is_holiday(tommorow) and not is_rain(tommorow_str):
# Google Calendarにイベントを追加
add_event(tommorow_str)
Google Calendarにイベントを追加する
最後に、Google Calendarにイベントを追加します。Google Calendar APIを有効化する方法は、こちらにわかりやすくまとまっているので、参考にしてください。詳細はリンク先に任せるとして、簡潔にAPIを使うまでの手順を説明します。
- Python Quickstart の「Enable the Google Calendar API」というボタンを押す。
- ポップアップが表示されたあと、「DOWNLOAD CLIENT CONFIGURATION」というボタンを押し、
credentials.json
をダウンロードする。 -
credentials.json
をプロジェクトと同じディレクトリに移動する。
とりあえずはこれで、APIを使う準備が整いました。
次に、追加したいカレンダーのIDを確認します。IDの確認方法も、先ほどのリンクで詳しく説明されているので、簡潔にフローを示します。
- Google Calendarを開く。
- 左のサイドバーの「マイカレンダー」のオーバーフローメニューから、追加したいカレンダーの「設定と共有」ボタンを押す。
- 「カレンダーの結合」の一番上の項目に「カレンダーID」が表示されているので、それをメモしておく。
確認したカレンダーIDを CALENDAR_ID
にセットすることで、ようやくイベントの追加を行うことができます。
import os.path
import pickle
from googleapiclient.discovery import build
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
CALENDAR_ID = 'Your Google Calendar ID'
DIR = os.path.dirname(os.path.abspath(__file__))
def add_event(tommorow):
creds = None
# The file token.pickle stores the user's access and refresh tokens, and is
# created automatically when the authorization flow completes for the first
# time.
token_pickle_path = os.path.join(DIR, 'token.pickle')
credentials_path = os.path.join(DIR, 'credentials.json')
if os.path.exists(token_pickle_path):
with open(token_pickle_path, 'rb') as token:
creds = pickle.load(token)
# If there are no (valid) credentials available, let the user log in.
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(credentials_path, SCOPES)
creds = flow.run_local_server(port=0)
# Save the credentials for the next run
with open(token_pickle_path, 'wb') as token:
pickle.dump(creds, token)
service = build('calendar', 'v3', credentials=creds)
# Call the Calndar API
event = {
'summary': 'ランニング',
'location': 'Your running location',
'start': {
'dateTime': '{}T07:30:00'.format(tommorow),
'timeZone': 'Japan',
},
'end': {
'dateTime': '{}T09:00:00'.format(tommorow),
'timeZone': 'Japan',
},
}
service.events().insert(calendarId=CALENDAR_ID, body=event).execute()
前半のコードに関しては、公式のサンプル からとってきたものなので、特に手を加える必要はなさそうです。あとは、 event
の summary
に追加したいイベントのタイトルを、 start
と end
の dateTime
にそれぞれ開始時間、終了時間を入れれば完成です。
完成コード
import datetime
import os.path
import pickle
import requests
import jpholiday
from googleapiclient.discovery import build
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
# If modifying these scopes, delete the file token.pickle.
SCOPES = ['https://www.googleapis.com/auth/calendar']
# 任意の値を設定してください。
CALENDAR_ID = 'Your Google Calendar ID'
OPENWEATHER_API_KEY = 'You Openweather API key'
ZIP_CODE = 'Zip code of your area'
DIR = os.path.dirname(os.path.abspath(__file__))
def add_event(tommorow):
creds = None
# The file token.pickle stores the user's access and refresh tokens, and is
# created automatically when the authorization flow completes for the first
# time.
token_pickle_path = os.path.join(DIR, 'token.pickle')
credentials_path = os.path.join(DIR, 'credentials.json')
if os.path.exists(token_pickle_path):
with open(token_pickle_path, 'rb') as token:
creds = pickle.load(token)
# If there are no (valid) credentials available, let the user log in.
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(credentials_path, SCOPES)
creds = flow.run_local_server(port=0)
# Save the credentials for the next run
with open(token_pickle_path, 'wb') as token:
pickle.dump(creds, token)
service = build('calendar', 'v3', credentials=creds)
event = {
'summary': 'ランニング',
'location': 'Your running location',
'start': {
'dateTime': '{}T07:30:00'.format(tommorow),
'timeZone': 'Japan',
},
'end': {
'dateTime': '{}T09:00:00'.format(tommorow),
'timeZone': 'Japan',
},
}
service.events().insert(calendarId=CALENDAR_ID, body=event).execute()
def is_rain(tommorow):
BASE_URL = 'http://api.openweathermap.org/data/2.5/forecast'
url = '{}?units=metric&zip={zip},JP&APPID={key}'.format(BASE_URL, zip=ZIP_CODE, key=OPENWEATHER_API_KEY)
res = requests.get(url).json()
weather_preds = res['list']
tommow_morning_dt = [tommorow + ' 06:00:00', tommorow + ' 09:00:00']
tommow_morning_weather = []
for pred in weather_preds:
if pred['dt_txt'] in tommow_morning_dt:
for weather in pred['weather']:
tommow_morning_weather.append(weather['main'])
return 'Rain' in tommow_morning_weather
def main():
tommorow = datetime.datetime.now() + datetime.timedelta(days=1)
tommorow_str = tommorow.strftime('%Y-%m-%d')
# weekday: 0 ~ 4
if tommorow.weekday() < 5 and not jpholiday.is_holiday(tommorow) and not is_rain(tommorow_str):
add_event(tommorow_str)
if __name__ == '__main__':
main()
なお、このコードを
$ python add_calendar.py
で実行した際に、1度目に警告画面が表示されますが、問題ないので、
- アカウントを選択
- 「詳細」リンクをクリック
- 「Quickstart(安全ではないページ)に移動」リンクをクリック
- 「許可」ボタンをクリック
- 「許可」ボタンをクリック
の手順で進めてください。2度目以降はこの操作を行う必要はありません。
ここまでで、明日の天気が雨でなく、平日であることを確認した上で、ランニングの予定を追加するスクリプトが完成したので、これを自動化したいと思います。
イベント追加の定期実行
定期実行については、macOSとLinuxにデフォルトで搭載されている crontab
を使えば簡単に行うことができます。詳しく知りたい方は、こちら を参考にしてください。まず、
$ crontab -e
で crontab
を立ち上げます。そして、このように書きます。
00 18 * * 0-4 [pythonのパス] [プロジェクトのディレクトリ]/add_calendar.py
pythonのパスは
$ which python
で確認することができます。
00 18 * * 0-4
というのは、日曜日〜木曜日の18時に実行するという意味です。左から順に、「分」、「時」、「日」、「月」、「曜日」を表しています。プログラム中で平日かどうかを判断しているため、曜日指定は必要ない気もしますが、少しでも無駄な処理を減らすために入れておきました。
まとめ
今回はランニングを追加する目的でスクリプトを書きましたが、条件を判断してカレンダーに予定を追加したいということは、割と頻繁にあると思うので、是非活用してみてください。
参考
pyenv と pyenv-virtualenv で環境構築
datetimeとjpholidayを組み合わせて、平日か土日祝日かを判定するスクリプト
pythonを使ってOpenWeatherMapから天気情報を取得
無料天気予報APIのOpenWeatherMapを使ってみる