M5Stack で Google Calendar のスケジュールを表示する(MicroPython)

 M5Stack でスケジュール管理に役立つ機能が実装できないかなと思い、 Google Calendar に登録しているスケジュールを表示させてみました。

Google Calendar API の利用設定

 まずは Google Calendar API を利用できるように設定する必要があります。GCP のコンソールから API とサービスを追加 をクリックします。

f:id:akanuma-hiroaki:20181117143616p:plain

 API のリストの中から Google Calendar API をクリックします。

f:id:akanuma-hiroaki:20181117143733p:plain

 API の詳細ページで 有効にする をクリックして API を使える状態にします。

f:id:akanuma-hiroaki:20181117143844p:plain

 次に API の認証情報を作成します。左メニューから 認証情報 をクリックします。

f:id:akanuma-hiroaki:20181117143949p:plain

 認証情報の種別を選択するプルダウンで サービスアカウントキー を選択します。

f:id:akanuma-hiroaki:20181117144053p:plain

 サービスアカウント名には任意の名前を設定します。役割については今回は参照だけできれば良いので、 閲覧者 を設定しました。サービスアカウント ID はサービスアカウント名から自動的に設定されます。キーのタイプはデフォルトが JSON になっているのでそのままにしておきます。最後に 作成 をすると認証情報が作成され、ダウンロードできるようになりますので、ローカルに取得しておきます。

f:id:akanuma-hiroaki:20181117161048p:plain

Google Calendar の共有設定

 次に先ほど作成したサービスアカウントからカレンダーを参照できるように、共有設定を行います。共有するカレンダーの共有設定画面から ユーザーの追加 をクリックします。

f:id:akanuma-hiroaki:20181117144504p:plain

 作成したサービスアカウントのサービスアカウント ID を設定します。権限は閲覧権限のみにしておきます。設定したら 送信 をクリックします。

f:id:akanuma-hiroaki:20181117144710p:plain

AWS Lambda の関数作成

 Google Calendar 側の設定はここまでで完了なので、次に AWS Lambda の関数を作成します。関数の作成画面で任意の関数名を指定します。ランタイムは今回は Python 3.6 を使用しています。

f:id:akanuma-hiroaki:20181117144835p:plain

 認証情報はコードの中に極力書きたくないので、 Lambda の実装画面で環境変数にサービスアカウントのキー ID を設定しておきます。

f:id:akanuma-hiroaki:20181117144930p:plain

 Google Calendar API を使うために Google が提供しているクライアントモジュールを使用します。 Lambda で外部モジュールを使用するにはローカルで zip に固めたものをアップロードする形になります。まずはローカルのプロジェクト用ディレクトリで pip を使用して google-api-python-client と oauth2client をインストールします。

$ pip3 install --upgrade google-api-python-client oauth2client -t ./

 下記のようなファイルがインストールされます。 Google Calendar API の認証情報作成時にダウンロードした認証情報ファイルも google_key.json として同じディレクトリに置いておきます。

$ ls -l
total 112
drwxr-xr-x   4 akanuma  staff    128 Nov 17 12:39 __pycache__
drwxr-xr-x   4 akanuma  staff    128 Nov 17 12:39 apiclient
drwxr-xr-x  12 akanuma  staff    384 Nov 17 12:39 cachetools
drwxr-xr-x   9 akanuma  staff    288 Nov 17 12:39 cachetools-3.0.0.dist-info
drwxr-xr-x   4 akanuma  staff    128 Nov 17 12:39 google
drwxr-xr-x   7 akanuma  staff    224 Nov 17 12:39 google_api_python_client-1.7.4.dist-info
-rw-r--r--   1 akanuma  staff    539 Nov 17 12:39 google_auth-1.6.1-py3.7-nspkg.pth
drwxr-xr-x   8 akanuma  staff    256 Nov 17 12:39 google_auth-1.6.1.dist-info
drwxr-xr-x   9 akanuma  staff    288 Nov 17 12:39 google_auth_httplib2-0.0.3.dist-info
-rw-r--r--   1 akanuma  staff   8434 Nov 17 12:39 google_auth_httplib2.py
-rw-r--r--@  1 akanuma  staff   2345 Nov 17 11:24 google_key.json
drwxr-xr-x  15 akanuma  staff    480 Nov 17 12:39 googleapiclient
drwxr-xr-x   8 akanuma  staff    256 Nov 17 12:39 httplib2
drwxr-xr-x   7 akanuma  staff    224 Nov 17 12:39 httplib2-0.12.0-py3.6.egg-info
-rw-r--r--   1 akanuma  staff    625 Nov 17 12:37 lambda_function.py
drwxr-xr-x  17 akanuma  staff    544 Nov 17 12:39 oauth2client
drwxr-xr-x   7 akanuma  staff    224 Nov 17 12:39 oauth2client-4.1.3.dist-info
drwxr-xr-x   9 akanuma  staff    288 Nov 17 12:39 pyasn1
drwxr-xr-x  11 akanuma  staff    352 Nov 17 12:39 pyasn1-0.4.4.dist-info
drwxr-xr-x  31 akanuma  staff    992 Nov 17 12:39 pyasn1_modules
drwxr-xr-x  11 akanuma  staff    352 Nov 17 12:39 pyasn1_modules-0.2.2.dist-info
drwxr-xr-x  19 akanuma  staff    608 Nov 17 12:39 rsa
drwxr-xr-x  11 akanuma  staff    352 Nov 17 12:39 rsa-4.0.dist-info
drwxr-xr-x   9 akanuma  staff    288 Nov 17 12:39 six-1.11.0.dist-info
-rw-r--r--   1 akanuma  staff  30888 Nov 17 12:39 six.py
drwxr-xr-x   7 akanuma  staff    224 Nov 17 12:39 uritemplate
drwxr-xr-x   9 akanuma  staff    288 Nov 17 12:39 uritemplate-3.0.0.dist-info

 この中で *.dist-info は不要なので削除しておきます。

$ rm -rf *.dist-info 

 削除後のリストは下記のようになります。

$ ls -l
total 112
drwxr-xr-x   4 akanuma  staff    128 Nov 17 12:39 __pycache__
drwxr-xr-x   4 akanuma  staff    128 Nov 17 12:39 apiclient
drwxr-xr-x  12 akanuma  staff    384 Nov 17 12:39 cachetools
drwxr-xr-x   4 akanuma  staff    128 Nov 17 12:39 google
-rw-r--r--   1 akanuma  staff    539 Nov 17 12:39 google_auth-1.6.1-py3.7-nspkg.pth
-rw-r--r--   1 akanuma  staff   8434 Nov 17 12:39 google_auth_httplib2.py
-rw-r--r--@  1 akanuma  staff   2345 Nov 17 11:24 google_key.json
drwxr-xr-x  15 akanuma  staff    480 Nov 17 12:39 googleapiclient
drwxr-xr-x   8 akanuma  staff    256 Nov 17 12:39 httplib2
drwxr-xr-x   7 akanuma  staff    224 Nov 17 12:39 httplib2-0.12.0-py3.6.egg-info
-rw-r--r--   1 akanuma  staff    625 Nov 17 12:37 lambda_function.py
drwxr-xr-x  17 akanuma  staff    544 Nov 17 12:39 oauth2client
drwxr-xr-x   9 akanuma  staff    288 Nov 17 12:39 pyasn1
drwxr-xr-x  31 akanuma  staff    992 Nov 17 12:39 pyasn1_modules
drwxr-xr-x  19 akanuma  staff    608 Nov 17 12:39 rsa
-rw-r--r--   1 akanuma  staff  30888 Nov 17 12:39 six.py
drwxr-xr-x   7 akanuma  staff    224 Nov 17 12:39 uritemplate

 これを zip に圧縮しておきます。

$ zip -r google_calendar_m5stack.zip ./*

 圧縮した zip ファイルを Lambda のコンソールからアップロードします。

 メインの関数(lambda_function.py)の内容は下記のようにしました。クラスの初期化時に認証情報を取得し、 get_schedules() メソッドで Google Calendar API をコールしてスケジュールの情報を取得しています。今回はとりあえず直近5件のスケジュールの開始日時、終了日時とサマリだけ使用しています。

from dateutil.parser import parse
from apiclient import discovery
from oauth2client.service_account import ServiceAccountCredentials
import datetime
import httplib2
import json
import logging
import os

logger = logging.getLogger()
logger.setLevel(logging.INFO)

class GoogleCalendar:
    def __init__(self):
        self.service_account_id = os.environ['GOOGLE_SERVICE_ACCOUNT_ID']
        scopes = 'https://www.googleapis.com/auth/calendar.readonly'
        
        self.credentials = ServiceAccountCredentials.from_json_keyfile_name(
            'google_key.json',
            scopes = scopes
        )
        
        self.calendar_id = 'XXXXXXXXXXXXXXX@gmail.com'
        self.max_results = 5

    def get_schedules(self):
        http = self.credentials.authorize(httplib2.Http())
        service = discovery.build('calendar', 'v3', http = http)
        
        now = datetime.datetime.utcnow().isoformat() + 'Z'
        
        events_result = service.events().list(
            calendarId   = self.calendar_id,
            timeMin      = now,
            maxResults   = self.max_results,
            singleEvents = True,
            orderBy      = 'startTime'
        ).execute()
        
        events = events_result.get('items', [])
        
        if not events:
            logger.info('No upcoming events found.')
        
        schedules = []
        for event in events:
            start   = event['start'].get('dateTime', event['start'].get('date'))
            end     = event['end'].get('dateTime', event['end'].get('date'))
            summary = event['summary']
            schedules.append({
                'start':   parse(start).strftime('%Y/%m/%d %H:%M:%S'),
                'end':     parse(end).strftime('%Y/%m/%d %H:%M:%S'),
                'summary': summary
            })

        return schedules

def lambda_handler(event, context):
    calendar = GoogleCalendar()
    schedules = calendar.get_schedules()
    
    return {
        'statusCode': 200,
        'body': json.dumps({'schedules': schedules})
    }

API Gateway の設定

 Lambda の関数が作成できたので、次に関数を API として実行できるように、 API Gateway の設定を行います。 AWS Lambda の実装画面でトリガーの追加メニューから API Gateway を選択します。

f:id:akanuma-hiroaki:20181117145413p:plain

 トリガーの設定フォームで API Gateway の設定が行えます。新規の API を作成するか既存の API から選択するかを選べますので、今回は 新規 API の作成 を選択します。セキュリティでは API キー使用でのオープン を選択して、 API キーで認証するようにしておきます。

f:id:akanuma-hiroaki:20181117145549p:plain

 下記のように未保存の状態で API が作成されますので、 Lambda コンソールの 保存 ボタンをクリックして設定を保存します。

f:id:akanuma-hiroaki:20181117145636p:plain

 すると実際に API が作成され、下記のように API の情報が表示されます。下記画像は色々とマスクしてあります。

f:id:akanuma-hiroaki:20181117145914p:plain

 試しに API キーなしで API にアクセスしてみると、 Forbidden となりアクセスが拒否されます。

$ curl https://XXXXXXXXXX.XXXXXXXXXXX.ap-northeast-1.amazonaws.com/default/googleCalendarM5Stack
{"message":"Forbidden"}

 API キーを Header に設定してリクエストを投げると下記のように情報を取得することができます。

$ curl https://XXXXXXXXXX.XXXXXXXXXXX.ap-northeast-1.amazonaws.com/default/googleCalendarM5Stack --header 'x-api-key:EnXb6oRB957iVzKXXXXXXXXXXXXXXXXXXXXXXXXX'
{"schedules": [{"start": "2018-11-18", "end": "2018-11-19", "summary": "\u6771\u4eac\u30aa\u30d5\u30a3\u30b9\u79fb\u8ee2"}, {"start": "2018-11-18", "end": "2018-11-19", "summary": "\u30bf\u30c3\u30d7\u516c\u6f14\u30ea\u30cf"}, {"start": "2018-11-22", "end": "2018-11-23", "summary": "SORACOM Technology Camp 2018"}, {"start": "2018-11-22T13:30:00+09:00", "end": "2018-11-22T19:30:00+09:00", "summary": "SORACOM Technology Camp 2018"}, {"start": "2018-11-23T12:00:00+09:00", "end": "2018-11-23T22:00:00+09:00", "summary": "TAP\u516c\u6f14\u30ea\u30cf"}]}

M5Stack のファームウェア実装(MicroPython)

 では最後に M5Stack 側の実装です。コードの全体は下記の通りです。主な処理は Lambda 側でやっているので、 M5Stack 側では単純に API を呼んで結果をループで表示しているだけのものになります。

from m5stack import lcd

import time
import ujson
import urequests

class GoogleCalendar:
    def __init__(self):
        self.base_url = 'https://XXXXXXXXXX.XXXXXXXXXXX.ap-northeast-1.amazonaws.com/default/googleCalendarM5Stack'
        self.api_key = 'EnXb6oRB957iVzKXXXXXXXXXXXXXXXXXXXXXXXXX'

        lcd.setCursor(0, 0)
        lcd.setColor(lcd.WHITE)
        lcd.font(lcd.FONT_DejaVu18)
        self.fw, self.fh = lcd.fontSize()

    def get_schedules(self):
        headers = {'x-api-key': self.api_key}
        response = urequests.get(self.base_url, headers = headers)
        json = response.json()
        return json['schedules']

    def display(self, schedules):
        lcd.clear()
        lcd.setCursor(0, 0)
        for schedule in schedules:
            print(schedule)
            lcd.println("{}".format(schedule['start']))
            lcd.println(" {}".format(schedule['summary']))

calendar = GoogleCalendar()
while True:
    schedules = calendar.get_schedules()
    calendar.display(schedules)
    time.sleep(60)

動作確認

 実行した結果は下記のようになります。表示は適当ですが、とりあえず Google Calendar の情報を M5Stack に表示することができました。ただ、日本語はそのままでは表示できませんので、テスト用のスケジュールを英語で登録して表示してみました。

f:id:akanuma-hiroaki:20181117155257j:plain

まとめ

 今回ひとまず Google Calendar から情報が取得できるようになりました。実際に使用するには表示を見やすく工夫したり、エラーハンドリングなどももっとちゃんと作りこむ必要がありますが、スケジュールの情報が使えると色々やれそうな気がします。また、今回 Lambda を経由して Google Calendar にアクセスすることで、 Google Calendar の認証情報は Lambda 側に保持し、デバイス上には持たせない構成になっているのはセキュリティ面では良い点かと思います。

 今回 Google Calendar API の使い方については下記チュートリアルを参考にしました。

Python Quickstart  |  Calendar API  |  Google Developers

 また、 Lambda で外部モジュールを使う方法については下記サイトを参考にさせていただきました。

qiita.com

 Lambda から Google Calendar API を利用する方法については下記サイトを参考にさせていただいています。

www.yamamanx.com