Pythonで明日ランニングするかどうかを自動で判断し、GoogleCalendarに追加するシステムを作った。

## はじめに
平日は基本的に、毎日ランニングをしているのですが、当然ながら雨の日や、休日に走る気はありません。
そこで、次の日走るかどうかの判断を自動化し、GoogleCalendarに予定を追加するプログラムをPythonで書いてみました。


## 環境
- macOS, Linux
- python 3.8.1

定期実行するのに `crontab` を使っているため、macOSかLinuxを想定していますが、WindowsではAWSなどを使えば、同じようなことができるかもしれません。


## Pythonの設定
まず、Pythonを設定します。個人的には、環境を汚すのが好きではないので、プロジェクト毎に `virtualenv` を作っています。本題とはずれるので、詳しくは[こちら](https://qiita.com/Kodaira_/items/feadfef9add468e3a85b)を参考にしてください。

```
$ 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](https://openweathermap.org/)を使用しました。

使い方は至ってシンプルで、アカウントを作成したあとの「API keys」というタブからAPIキーを取得し、`OPENWEATHER_API_KEY` にセットします。
ちなみに、今回は場所の指定に郵便番号を使いましたが、都市名や、緯度経度でも指定することが[できるようです](https://qiita.com/nownabe/items/aeac1ce0977be963a740)。

```python
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()
```

すると、このような結果が返ってきます。

```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` に着目したいと思います。

```python
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` に追加しています。

これで明日のランニングをする時間帯の天気を取得することができました。しかし、この実装は正直微妙だと思っていて、日付を文字列で比較するのはナンセンスな気がするので、いい方法があったら教えてください。


## 休日かどうかを判断する
基本的に、平日以外は走る気がないので、休日は除外したいです。単に休日を除外するだけであれば話は簡単なのですが、祝日についても考慮しなければならないので、[こちら](https://qiita.com/hid_tanabe/items/3c5e6e85c6c65f7b38be)を参考に、 `jpholiday` というパッケージを使いました。先ほど作った雨かどうかを判定する `is_rain()` も使用することで、ランニングをするときの条件を表すことができます。
ちなみに、 `datetime` の `weekday()` という関数は、月〜金が 0~4 に対応しており、土日は 5,6 に対応しています。

```python
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を有効化する方法は、[こちら](https://non-dimension.com/python-googlecalendarapi/)にわかりやすくまとまっているので、参考にしてください。詳細はリンク先に任せるとして、簡潔にAPIを使うまでの手順を説明します。

1. [Python Quickstart](https://developers.google.com/calendar/quickstart/python?hl=ja) の「Enable the Google Calendar API」というボタンを押す。
2. ポップアップが表示されたあと、「DOWNLOAD CLIENT CONFIGURATION」というボタンを押し、 `credentials.json` をダウンロードする。
3. `credentials.json` をプロジェクトと同じディレクトリに移動する。

とりあえずはこれで、APIを使う準備が整いました。

次に、追加したいカレンダーのIDを確認します。IDの確認方法も、先ほどのリンクで詳しく説明されているので、簡潔にフローを示します。

1. Google Calendarを開く。
2. 左のサイドバーの「マイカレンダー」のオーバーフローメニューから、追加したいカレンダーの「設定と共有」ボタンを押す。
3. 「カレンダーの結合」の一番上の項目に「カレンダーID」が表示されているので、それをメモしておく。

確認したカレンダーIDを `CALENDAR_ID` にセットすることで、ようやくイベントの追加を行うことができます。

```python
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()
```

前半のコードに関しては、[公式のサンプル](https://github.com/gsuitedevs/python-samples/blob/master/calendar/quickstart/quickstart.py) からとってきたものなので、特に手を加える必要はなさそうです。あとは、 `event` の `summary` に追加したいイベントのタイトルを、 `start` と `end` の `dateTime` にそれぞれ開始時間、終了時間を入れれば完成です。


## 完成コード

```python:add_calendar.py
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. アカウントを選択
2. 「詳細」リンクをクリック
3. 「Quickstart（安全ではないページ）に移動」リンクをクリック
4. 「許可」ボタンをクリック
5. 「許可」ボタンをクリック

の手順で進めてください。２度目以降はこの操作を行う必要はありません。

ここまでで、明日の天気が雨でなく、平日であることを確認した上で、ランニングの予定を追加するスクリプトが完成したので、これを自動化したいと思います。

## イベント追加の定期実行
定期実行については、macOSとLinuxにデフォルトで搭載されている `crontab` を使えば簡単に行うことができます。詳しく知りたい方は、[こちら](https://www.server-memo.net/tips/crontab.html) を参考にしてください。まず、

```
$ crontab -e
```

で `crontab` を立ち上げます。そして、このように書きます。

```
00 18 * * 0-4 [pythonのパス] [プロジェクトのディレクトリ]/add_calendar.py
```

pythonのパスは

```
$ which python
```

で確認することができます。

`00 18 * * 0-4` というのは、日曜日〜木曜日の18時に実行するという意味です。左から順に、「分」、「時」、「日」、「月」、「曜日」を表しています。プログラム中で平日かどうかを判断しているため、曜日指定は必要ない気もしますが、少しでも無駄な処理を減らすために入れておきました。


## まとめ
今回はランニングを追加する目的でスクリプトを書きましたが、条件を判断してカレンダーに予定を追加したいということは、割と頻繁にあると思うので、是非活用してみてください。


## 参考
[pyenv と pyenv-virtualenv で環境構築
](https://qiita.com/Kodaira_/items/feadfef9add468e3a85b)

[datetimeとjpholidayを組み合わせて、平日か土日祝日かを判定するスクリプト
](https://qiita.com/hid_tanabe/items/3c5e6e85c6c65f7b38be)

[pythonを使ってOpenWeatherMapから天気情報を取得
](https://qiita.com/optimisuke/items/87d4846de15736b599e4)

[無料天気予報APIのOpenWeatherMapを使ってみる
](https://qiita.com/nownabe/items/aeac1ce0977be963a740)

[PythonでGoogleカレンダーに予定を追加する
](https://qiita.com/hajime_migi/items/d7d0a310995a99297e80)

[Google Calendar APIを使ってGoogle Calendarの予定を取得・追加する
](https://non-dimension.com/python-googlecalendarapi/)

[crontabの書き方
](https://www.server-memo.net/tips/crontab.html)