West Gate Laboratory

人生を少しでも,面白く便利にするモノづくり

Google Calendarの予定に応じてRaspberryPiに処理をさせる(その1)

はじめに

この記事はオートロックマンション用不在時荷物受け取りシステムの開発記事です。

westgate-lab.hatenablog.com

前回までに、RaspberryPi+ウェブカメラ+サーボモータを使って「インターホン音検知」→「インターホンの解錠ボタン押下」までできた。しかしながら、このままではインターホンを押した人は誰でも入れるガバガバセキュリティ状態になってしまう。

そこで、今回はRaspberryPiとGoogle Calendarを連携させ、ユーザがカレンダー上で指定した時間帯のみ、インターホンの音に応じて解錠するように改良する。例えば、あらかじめ時間指定した配達であれば、その時間をGoogle Calendar上に登録しておく。また時間指定はできないが日付が決まっている場合は、終日イベントとして登録しておく。RaspberryPiはそれに応じてインターホンに応答する、という具合。

RaspberryPi:Model 3B+

Python:3.7.3

RaspberryPiとGoogle Calendarの連携

PythonからGoogle Calendarに情報を取りに行く方法は、以下のGoogle Calendar APIPython Quickstartにすべて手順が載っている。基本的にその手順に従っていけば良い。

developers.google.com

RaspberryPiからカレンダー情報を取りに行く上で重要な点を踏まえつつ手順を述べる。開発環境はPCからSSHでRaspberryPiに接続していることを前提としているが、最初のSTEP1まではPC上で作業したほうが楽である。

STEP1:Google Calendar API有効化

まず、当たり前だがgoogleアカウントを用意し、情報を取りに行くカレンダーを作成する。もともと持っているならそれでOK。そのアカウントでログインしておく。

次に上記QuickstartのSTEP1を行う。STEP1にある「Enable the Google Calendar API」をクリックし、APIを有効化する。

f:id:kaname_m:20200104125713p:plain
STEP1:Google Calendar APIを有効化

有効化がうまく行けば、以下の画面が表示される。「Download Client Configuration」からコンフィギュレーションファイルをダウンロードする。credentials.jsonという名前だ。

f:id:kaname_m:20200104125847p:plain
STEP1:API有効化完了。コンフィギュレーションファイルをダウンロードしておく

もしPCでcredentials.jsonをダウンロードしたなら、そのファイルをRaspberryPiのワーキングディレクトリにWinSCP等使ってコピーしておく。

STEP2:必要なライブラリのインストール

ここから先はRaspberryPiでの作業だ。 credentials.jsonがRaspberryPiに用意できたら、Pythonの必要なライブラリをRaspberryPiにインストールする。これもquickstartのSTEP2の通りだが、

pip3 install --upgrade google-api-python-client google-auth-httplib2 google-auth-oauthlib

でインストールできる(Python3を使っているのでpip3)。

STEP3:RaspberryPiからGoogle Calendarへのアクセスを許可する

ライブラリがインストールできたら、情報を取得しに行くわけだが、最初に一度認証を行う必要がある。 認証に使うスクリプトはquickstartにあるので、それを使う。

先程credentials.jsonを保存したRaspberryPiのワーキングディレクトリにquickstart STEP3にあるサンプルファイルを保存する。例えばwgetを使えば以下のコマンドで取得できる。

wget https://raw.githubusercontent.com/gsuitedevs/python-samples/master/calendar/quickstart/quickstart.py

この先、認証のためにGUIが必要になるため、以下の記事などを参考にXwindowでGUI環境が使えるようにしておく。

Raspberry Pi 2(略してパイ2)のGUI環境をWindows10から使う - Qiita

サンプルが保存できたら、python3 quickstart.pyで早速サンプル実行する。 すると、認証URLが表示されるため、クリックしてブラウザでそのURLにアクセスする(ここでGUIが必要になる)。

googleアカウントの認証画面が出るので、Google Calendarに紐付いたアカウント名、パスワードを入力し、画面に従って許可をする。

問題なく終わればその旨表示され、ワーキングディレクトリにtoken.pickleというファイルができているはずである。それを確認したらブラウザは閉じて構わない。

STEP4:サンプル実行

さて、ここまで来たらいよいよGoogle Calendarからの情報取得である。

実は、先程認証に使ったquickstart.pyがそのまま直近10個の予定を取得するサンプルスクリプトにもなっているpython3 quickstart.pyで実行してみよう。直近の予定が表示されるはずだ。日本語の予定を取得する場合は文字コードに注意する。(自分の環境ではEUC-JPで正しく表示できた)

ここまでがうまくできたらあとは自分のスクリプトに組み込んでいく。

APIの実際の使い方

Pythonで使いやすいように、quickstart.pyを改造してクラス化した。まずはそれを掲載する。今回の不在時荷物受け取りシステムでは、深夜0時にその日の予定をRaspberryPiから取得しに行くため、get_today_schedule()という関数を作った。また、最初に一度必ず必要なログイン処理はlogin()という単独の関数とした。

from datetime import datetime, date, time, timedelta
from pytz import timezone
import pickle
import os.path
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.readonly']

class googleCalendarApi:
    def __init__(self):
        self.jst = timezone('Asia/Tokyo')

    def login(self):
        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.
        if os.path.exists('token.pickle'):
            with open('token.pickle', '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.json', SCOPES)
                creds = flow.run_local_server(port=0)
            # Save the credentials for the next run
            with open('token.pickle', 'wb') as token:
                pickle.dump(creds, token)

        self.creds = creds

    def logout(self):
        pass

    def get_today_schedule(self, remain=False):
        service = build('calendar', 'v3', credentials=self.creds)

        # Call the Calendar API
        now = datetime.now()
        if remain:
            begin_time = now
        else:
            begin_time = datetime.combine(now, time(0,0))
        begin_time = self.jst.localize(begin_time)
        end_time = datetime.combine(now, time(23,59,59))
        end_time = self.jst.localize(end_time)

        events_result = service.events().list(calendarId='primary', timeMin=begin_time.isoformat(),
                timeMax=end_time.isoformat(), singleEvents=True,
                orderBy='startTime').execute()
        events = events_result.get('items', [])

        event_list = []
        if not events:
            print('No upcoming events found.')
        for event in events:
            st = event['start'].get('dateTime')
            if st == None:
                st = event['start'].get('date')
                en = event['end'].get('date')
                start_date = datetime.strptime(st, '%Y-%m-%d')
                end_date = datetime.strptime(en, '%Y-%m-%d')
                start_date = start_date + timedelta(hours = 9)  # all-day event starts 9AM
                end_date = end_date + timedelta(hours = -3)     # all-day event ends 21PM
                ev = {"summary":event['summary'], "all-day":True, "start":start_date, "end":end_date}
            else:
                en = event['end'].get('dateTime')
                start_date = datetime.strptime(st, '%Y-%m-%dT%H:%M:%S+09:00')
                end_date = datetime.strptime(en, '%Y-%m-%dT%H:%M:%S+09:00')
                ev = {"summary":event['summary'], "all-day":False, "start":start_date, "end":end_date}
            event_list.append(ev)

        return event_list

Google Calendar APIの使い方については、以下のReferenceを参照。

developers.google.com

例えば、具体的にカレンダーの予定を取得しているのは以下の部分である。

        events_result = service.events().list(calendarId='primary', timeMin=begin_time.isoformat(),
                timeMax=end_time.isoformat(), singleEvents=True,
                orderBy='startTime').execute()
        events = events_result.get('items', [])

取得開始時刻と終了時刻をISOフォーマットで指定して、開始時間順で取得する。

その次にそれらのイベントの必要な情報のみをリスト化する。ここで注意なのは、時間指定のイベントと週日イベントではイベント開始時刻や終了時刻の扱いに差がある点である。

具体的には、終日イベントには開始時刻event['start'].get('dateTime')や終了時刻event['end'].get('dateTime')がなく、Noneが入っている。そのため、終日イベントの場合は、実際に配達が行われる可能性のある、AM9時~PM9時までとした。以下がその部分。

            if st == None:
                st = event['start'].get('date')
                en = event['end'].get('date')
                start_date = datetime.strptime(st, '%Y-%m-%d')
                end_date = datetime.strptime(en, '%Y-%m-%d')
                start_date = start_date + timedelta(hours = 9)  # all-day event starts 9AM
                end_date = end_date + timedelta(hours = -3)     # all-day event ends 21PM
                ev = {"summary":event['summary'], "all-day":True, "start":start_date, "end":end_date}

そして、このGoogleCalendarApiクラスをインポートして使う。 例えば、その日の予定の名から「配達」という名前のイベントだけ抽出し、その開始時刻と終了時刻をファイル(schedule.txt)に書き出すスクリプトは以下のようになる。

import googleCalendarApi

fpath = 'ワーキングディレクトリ'
fname = 'schedule.txt'

cl = googleCalendarApi.googleCalendarApi()
cl.login()
events = cl.get_today_schedule(remain = True)

f = open(fpath+fname, 'w')
for event in events:
    if event["summary"] == "配達":
        start = event["start"].strftime('%Y-%m-%dT%H:%M:%S')
        end = event["end"].strftime('%Y-%m-%dT%H:%M:%S')
        f.write(start + " " + end + "\n")

f.close()

これをCRONに登録して毎日深夜0時に呼び出すことで、その日の予定をテキストに吐き出している。CRONで呼ぶ場合はワーキングディレクトリの部分を絶対パスで記述すること。

まとめ

Google Calendar APIを使い、RaspberryPiからGoogle Calendarの予定を取得する方法を述べた。基本はquickstartに従っていけば良い。

次回は、これを使って作成したスケジュールのテキストファイルを実際に使ってインターホンの応答/非応答を行う。