Google Calendarから予定を取得してESP32のLCDに表示する

前回の投稿 [Google Photosからランダムな1枚の画像を取得できるようにする](https://qiita.com/poruruba/items/ac4f12022da1173b575e) にて、Google Photosに登録しておいた画像をランダムに表示されるフォトフレームを作りました。
今回は、少し拡張して、Google Calendarに登録しておいたイベントも合わせて表示するようにします。

Google PhotosのAPIの呼び出しの時には、REST呼び出しでしたが、Google CalendarはNode.js用のライブラリが用意されているのでそれを使います。
ですが、Googleアカウントログイン部分は、REST呼び出しのものを流用したいので、REST呼び出しで認証したGoogleアカウントの認証結果(アクセストークン)をGoogle Calendarライブラリに引き継ぐようにします。

ソースコードもろもろは、以下に上書きしています。

poruruba/GooglePhotosGallery

https://github.com/poruruba/GooglePhotosGallery

#Google Calendar APIを利用できるようにする

Google Cloud Platformのコンソールから、Google Calendar APIを使えるように有効化します。

GCP：APIとライブラリ
https://console.cloud.google.com/apis/library

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/9124bab6-7365-c585-a838-d52a7f6b533e.png)

検索のところに、Calendarと入力すると、Google Calendar APIが出てきますので、選択してEnableにします。

#認証結果をGoogleライブラリに引き継ぐ

以前の投稿 [GoogleAPIライブラリを使わずにGoogleアカウントでログインできるようにする](https://qiita.com/poruruba/items/0f99400d178c4497f79b) で認証結果(アクセストークン・IDトークン・リフレッシュトークン)をファイル出力していました。
それを使います。

```js:node.js/api/controllers/googlecalendar-api/index.js
function get_calendar(token){
  const oAuth2Client = new google.auth.OAuth2(CLIENT_ID);
  oAuth2Client.setCredentials(token);
  return google.calendar({ version: 'v3', auth: oAuth2Client });
}
```

上記のtokenには、access_tokenがあれば大丈夫です。
ただし、このトークンを取得したときのGoogleアカウント認証時のScopeに、以下が含まれている必要があります。

``` https://www.googleapis.com/auth/calendar.readonly ```

CLIENT_IDは、認証時に利用していたクライアントIDを指定してください。

あとは、以下のように呼び出すだけです。

```js:node.js/api/controllers/googlecalendar-api/index.js
  var result = await new Promise((resolve, reject) => {
    calendar.events.list(params, (err, res) => {
      if (err)
        return reject(err);
      resolve(res);
    });
  });
```

paramsには、以下で示されるパラメータを指定します。

https://developers.google.com/calendar/api/v3/reference/events/list?hl=ja

取得結果には、nextPageTokenが含まれている場合があります。その場合には、すべてのイベントを取得しきれていないので、pageTokenにnextPageTokenを指定して、続けて呼び出す必要があります。

結局、こんな感じです。

```js:node.js/api/controllers/googlecalendar-api/index.js
async function get_event_list(calendar, date){
  var startTime = date.toISOString();
  var endDate = new Date(date.getTime());
  endDate.setHours(23);
  endDate.setMinutes(59);
  endDate.setSeconds(59);
  endDate.setMilliseconds(999);
  var endTime = endDate.toISOString();

  var params = {
    calendarId: 'primary',
    timeMin: startTime,
    timeMax: endTime,
    singleEvents: true,
    orderBy: 'startTime',
  };
  var result = await new Promise((resolve, reject) => {
    calendar.events.list(params, (err, res) => {
      if (err)
        return reject(err);
      resolve(res);
    });
  });
  var items = result.data.items || [];
  while (result.nextPageToken) {
    params.pageToken = result.nextPageToken;
    result = await new Promise((resolve, reject) => {
      calendar.events.list(params, (err, res) => {
        if (err)
          return reject(err);
        resolve(res);
      });
    });
    items = items.concat(result.data.items);
  }

  var list = [];
  for (const item of items) {
    var term = convert_date(item.start, item.end);
    list.push({
      summary: item.summary,
      term: term
    });
  }

  return list;
}
```

#イベントの変更通知を受け取る

Google Calendarのイベントは、他のGoogleアプリから編集されるため、イベントリスト取得後にも変更されている場合があります。
そこで、変更されたことをNotificationとして通知してくれる機能があります。以下のように登録します。

(参考)
https://developers.google.com/calendar/api/guides/push?hl=ja

```js:node.js/api/controllers/googlecalendar-api/index.js
const CALENDAR_WEBHOOK_URL = 'https://【Node.jsサーバのURL】/googlecalendar-webhooks';
・・・
      var params = {
        id: uuidv4(),
        type: "web_hook",
        address: CALENDAR_WEBHOOK_URL
      };
      var result = await do_post_with_token('https://www.googleapis.com/calendar/v3/calendars/primary/events/watch', params, token.access_token); 
```

URLのところで「primary」に指定することで、ログインユーザのプライマリカレンダーが変更検知対象になります。
idには、毎回異なる値を指定します。今回はuuidにしました。
addressには、通知を受けたいWebAPIのURLを指定します。

戻り値はJsonです。戻り値のresultには、idとresourceIdが含まれており、登録解除時に必要ですので、ファイルに保存しておきます。

解除は以下の通りです。

```js:node.js/api/controllers/googlecalendar-api/index.js
      var params = {
        id: json.notification.id,
        resourceId: json.notification.resourceId
      };
      var result = await do_post_text_with_token('https://www.googleapis.com/calendar/v3/channels/stop', params, token.access_token);
      console.log(result);
```

戻り値は、Jsonではなくテキストです。

以下は、通知を受けた時の処理の例です。

```js:node.js/api/controllers/googlecalendar-api/index.js
    case '/googlecalendar-webhooks': {
      console.log(event);
      if( event.headers['x-goog-resource-state'] == 'exists'){
        var token = await read_token();
        const calendar = get_calendar(token);
        var list = await read_event_list(calendar, true);

        client.publish(TOPIC_CMD, "1");
      }
      return new Response({});
    }
```

登録したURLに届く通知は、Google Calendarのイベントの変更時以外にも、いくつかのタイミングで届きます。HTTPヘッダー「x-goog-resource-state」がexistsの時が、イベント変更時の通知です。

上記の例では、イベントリストを更新したのち、MQTTにパブリッシュして他のデバイス(今回の場合は、LCD付のESP32がSubscribeしている)に通知しています。

#ついでにGoogle Tasks(toDo)のタスクリストも取得してみます

こちらも、Googleライブラリ化されていますので、それを使います。
毎度の通り、以下で、Tasks APIを有効にします。

GCP：APIとライブラリ
https://console.cloud.google.com/apis/library

また、Googleアカウント認証時には、以下をScopeに含めます。

``` https://www.googleapis.com/auth/tasks.readonly ```

あとは、以下のようにtasksのインスタンスを生成して、呼び出すだけです。

```js:node.js/api/controllers/googlecalendar-api/index.js
    case '/googletasks-list': {
      var token = await read_token();
      const tasks = get_tasks(token);
      var list = await get_tasklist_list(tasks);
      console.log(list);
      for( var i = 0 ; i < list.length ; i++ ){
        list[i].list = await get_task_list(tasks, list[i].id);
      }
      return new Response({ list: list });
    }
・・・
function get_tasks(token) {
  const oAuth2Client = new google.auth.OAuth2(CLIENT_ID);
  oAuth2Client.setCredentials(token);
  return google.tasks({ version: 'v1', auth: oAuth2Client });
}

async function get_tasklist_list(tasks){
  var params = {};
  var result = await new Promise((resolve, reject) => {
    tasks.tasklists.list(params, (err, res) => {
      if (err)
        return reject(err);
      resolve(res);
    });
  });
  var items = result.data.items;
  while (result.nextPageToken) {
    params.pageToken = result.nextPageToken;
    result = await new Promise((resolve, reject) => {
      tasks.tasklists.list(params, (err, res) => {
        if (err)
          return reject(err);
        resolve(res);
      });
    });
    items = items.concat(result.data.items);
  }

  var list = [];
  for (const item of items) {
    list.push({
      title: item.title,
      id: item.id
    });
  }

  return list;
}

async function get_task_list(tasks, id) {
  var params = {
    tasklist: id
  };
  var result = await new Promise((resolve, reject) => {
    tasks.tasks.list(params, (err, res) => {
      if (err)
        return reject(err);
      resolve(res);
    });
  });
  var items = result.data.items || [];
  while (result.nextPageToken) {
    params.pageToken = result.nextPageToken;
    result = await new Promise((resolve, reject) => {
      tasks.tasks.list(params, (err, res) => {
        if (err)
          return reject(err);
        resolve(res);
      });
    });
    items = items.concat(result.data.items);
  }

  var list = [];
  for (const item of items) {
    list.push({
      title: item.title,
      notes: item.notes,
      id: item.id,
      parent: item.parent || null,
      due: item.due ? new Date(item.due).getTime() : null
    });
  }

  return list;
}
```

流れとしては、タスクリストというタスクを束ねたもののリストを取得したのち、タスクリストごとに含まれるタスクのリストを取得する、という流れです。

流れさえわかれば、おおよそ理解できると思いますので、詳しくは、GitHubのソースコード参照してください。

poruruba/GooglePhotosGallery
　https://github.com/poruruba/GooglePhotosGallery

#LCD付ESP32の実装

Google Calendarのイベントの取得は、以下のURLに対してJSON-POST呼び出ししているだけです。

``` https://【Node.jsサーバのホスト名】/googlecalendar-list ```

一方で、イベントの変更検知するために、MQTTでトピック「calendar/notify」でSubscribeして待ち受けています。通知を受けたら、Google Calendarイベントの再取得を含むLCD再描画処理を実行しています。

詳しくは、GitHubのソースコード参照してください。

poruruba/GooglePhotosGallery
　https://github.com/poruruba/GooglePhotosGallery

以上