Power Appsで会議室の空き状況を表示する

Power Automate からプレミアムコネクタなしで Graph API が利用できるので
Graph API を利用して、会議室の予約情報を参照し、iPad などで表示するアプリケーションを Power Apps で作成してみる。

## Power Apps の作成
空のキャンバスアプリから作成。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2739358/0427e381-84c0-e702-b64d-a29f6873419d.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2739358/cc477c40-27e0-4000-9a89-a1a262f55b34.png)
Power Automateの新しいフローを作成する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2739358/27747b56-1d14-def8-b4de-761f4c2ab8f5.png)
## Power Automate の作成
### 全体の流れ
Power Appsから会議室のメールアドレスを引数に会議情報を返却するフローのサンプルを以下に記載する。
詳細については以降で細かく説明。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2739358/dcf06b53-83c7-f7a2-fd01-88c879537789.png)

### 1. PowerApps (V2)
今回のフローは PowerApps (V2) をトリガーに動作する。
引数として会議室のメールアドレスを受け取るため、その変数名と説明を記載する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2739358/393098b3-e07c-07dc-3dc3-d574a3f02c32.png)

:::note warn
Power Apps からの作成時にV2で作成されない場合、一度 Power Apps のトリガーを削除し、V2で再作成する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2739358/f8b88911-f0d6-71e4-374b-f48c11842069.png)
:::

### 2. 変数を初期化する（返却値）
会議室が使用中の場合も空室の場合も同じ値を返却するためにオブジェクトとして定義する。
正直、文字列変数でもどちらでもいい。
拡張性（例えば、複数の会議室情報を返却する・複数の会議予定を返却する等）を考えるとオブジェクトのほうが良いかと個人的には思う。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2739358/7cc53182-c1e0-b71f-c013-e3be9ddcf031.png)

### 3. 現在時刻の書式変更
この後のGraph APIを呼び出す時の時刻書式を変更するためだけに利用。
もう少しいいやり方はあるかもしれない・・・。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2739358/e117f925-ac86-546a-7d43-50b98c116063.png)

### 4. GraphAPIで会議室の予約状況を取得する
Office 365 Groups を利用して、GraphAPI を呼び出す。ここがこのフローのメイン。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2739358/af3c72f4-f18d-c610-43a0-5205297cf006.png)

今回は以下のAPIを使用
https://learn.microsoft.com/ja-jp/graph/api/user-list-calendarview?view=graph-rest-1.0

URIの中に設定した値はこんな感じ。

```
concat('https://graph.microsoft.com/v1.0/users/',triggerBody()['text'],'/calendar/calendarView?startDateTime=',body('現在時刻の書式変更'),'&endDateTime=',body('現在時刻の書式変更'),'&orderby=start/dateTime&top=1')
```
複数件取れることはないだろうが念のため、開始時間でソート（orderby=start/dateTime）し、最初の1件目（top=1）を取得するようにしている。

各変数にはそれぞれ以下の内容が入る。
|変数|内容|
|:---|:---|
|triggerBody()['text']|1. PowerApps (V2) で取得した引数となるメールアドレス|
|body('現在時刻の書式変更')|3. 現在時刻の書式変更 で変換した現在時刻書式<br>API仕様上、時刻の書式は **ISO 8601** でタイムゾーンは **UTC** で解釈されるため注意。|

### 5. JSON の解析（GraphAPI取得結果）
これが厄介。特にスキーマ情報は公開されていなかったので、[Graphエクスプローラー](https://developer.microsoft.com/en-us/graph/graph-explorer) を使いながら微調整。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2739358/bad98a48-726b-2b1c-13c2-967087f6739e.png)
参考になるかはわからないが、定義した内容は以下。

<details><summary>スキーマ（クリックで開く）</summary>

```
{
    "type": "object",
    "properties": {
        "@@odata.context": {
            "type": "string"
        },
        "value": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "@@odata.etag": {
                        "type": "string"
                    },
                    "id": {
                        "type": "string"
                    },
                    "createdDateTime": {
                        "type": "string"
                    },
                    "lastModifiedDateTime": {
                        "type": "string"
                    },
                    "changeKey": {
                        "type": "string"
                    },
                    "categories": {
                        "type": "array"
                    },
                    "transactionId": {},
                    "originalStartTimeZone": {
                        "type": "string"
                    },
                    "originalEndTimeZone": {
                        "type": "string"
                    },
                    "iCalUId": {
                        "type": "string"
                    },
                    "reminderMinutesBeforeStart": {
                        "type": "integer"
                    },
                    "isReminderOn": {
                        "type": "boolean"
                    },
                    "hasAttachments": {
                        "type": "boolean"
                    },
                    "subject": {
                        "type": "string"
                    },
                    "bodyPreview": {
                        "type": "string"
                    },
                    "importance": {
                        "type": "string"
                    },
                    "sensitivity": {
                        "type": "string"
                    },
                    "isAllDay": {
                        "type": "boolean"
                    },
                    "isCancelled": {
                        "type": "boolean"
                    },
                    "isOrganizer": {
                        "type": "boolean"
                    },
                    "responseRequested": {
                        "type": "boolean"
                    },
                    "seriesMasterId": {},
                    "showAs": {
                        "type": "string"
                    },
                    "type": {
                        "type": "string"
                    },
                    "webLink": {
                        "type": "string"
                    },
                    "onlineMeetingUrl": {},
                    "isOnlineMeeting": {
                        "type": "boolean"
                    },
                    "onlineMeetingProvider": {
                        "type": "string"
                    },
                    "allowNewTimeProposals": {
                        "type": "boolean"
                    },
                    "occurrenceId": {},
                    "isDraft": {
                        "type": "boolean"
                    },
                    "hideAttendees": {
                        "type": "boolean"
                    },
                    "responseStatus": {
                        "type": "object",
                        "properties": {
                            "response": {
                                "type": "string"
                            },
                            "time": {
                                "type": "string"
                            }
                        }
                    },
                    "body": {
                        "type": "object",
                        "properties": {
                            "contentType": {
                                "type": "string"
                            },
                            "content": {
                                "type": "string"
                            }
                        }
                    },
                    "start": {
                        "type": "object",
                        "properties": {
                            "dateTime": {
                                "type": "string"
                            },
                            "timeZone": {
                                "type": "string"
                            }
                        }
                    },
                    "end": {
                        "type": "object",
                        "properties": {
                            "dateTime": {
                                "type": "string"
                            },
                            "timeZone": {
                                "type": "string"
                            }
                        }
                    },
                    "location": {
                        "type": "object",
                        "properties": {
                            "displayName": {
                                "type": "string"
                            },
                            "locationType": {
                                "type": "string"
                            },
                            "uniqueIdType": {
                                "type": "string"
                            },
                            "address": {
                                "type": "object",
                                "properties": {}
                            },
                            "coordinates": {
                                "type": "object",
                                "properties": {}
                            }
                        }
                    },
                    "locations": {
                        "type": "array"
                    },
                    "recurrence": {},
                    "attendees": {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "properties": {
                                "type": {
                                    "type": "string"
                                },
                                "status": {
                                    "type": "object",
                                    "properties": {
                                        "response": {
                                            "type": "string"
                                        },
                                        "time": {
                                            "type": "string"
                                        }
                                    }
                                },
                                "emailAddress": {
                                    "type": "object",
                                    "properties": {
                                        "name": {
                                            "type": "string"
                                        },
                                        "address": {
                                            "type": "string"
                                        }
                                    }
                                }
                            },
                            "required": [
                                "type",
                                "status",
                                "emailAddress"
                            ]
                        }
                    },
                    "organizer": {
                        "type": "object",
                        "properties": {
                            "emailAddress": {
                                "type": "object",
                                "properties": {
                                    "name": {
                                        "type": "string"
                                    },
                                    "address": {
                                        "type": "string"
                                    }
                                }
                            }
                        }
                    },
                    "onlineMeeting": {}
                },
                "required": [
                    "@@odata.etag",
                    "id",
                    "createdDateTime",
                    "lastModifiedDateTime",
                    "changeKey",
                    "categories",
                    "transactionId",
                    "originalStartTimeZone",
                    "originalEndTimeZone",
                    "iCalUId",
                    "reminderMinutesBeforeStart",
                    "isReminderOn",
                    "hasAttachments",
                    "subject",
                    "bodyPreview",
                    "importance",
                    "sensitivity",
                    "isAllDay",
                    "isCancelled",
                    "isOrganizer",
                    "responseRequested",
                    "seriesMasterId",
                    "showAs",
                    "type",
                    "webLink",
                    "onlineMeetingUrl",
                    "isOnlineMeeting",
                    "onlineMeetingProvider",
                    "allowNewTimeProposals",
                    "occurrenceId",
                    "isDraft",
                    "hideAttendees",
                    "responseStatus",
                    "body",
                    "start",
                    "end",
                    "location",
                    "locations",
                    "recurrence",
                    "attendees",
                    "organizer",
                    "onlineMeeting"
                ]
            }
        }
    }
}
```
</details>

### 6. 繰り返し（JSON）
JSON の解析（GraphAPI取得結果）で解析した結果から返却値を作成する。
Graph API で取得した結果はUTCなので会議の開催開始時間、終了時間をそれぞれJSTに変換する。
変換後は変数に設定する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2739358/96641d5d-6905-0d08-8e99-d73262b1c456.png)

:::note warn
dateTimeは複数あるので注意。['start']?['dateTime'] が開始時間、['end']?['dateTime'] が終了時間となる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2739358/49c8ac80-f221-8e18-4b46-cec42312acb1.png)
:::

### 7. JSON の解析（返却値）
返却値の変数がオブジェクトなので解析する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2739358/0ce9abd5-cc70-7299-2371-ef8562916091.png)

<details><summary>スキーマ（クリックで開く）</summary>

```
{
    "type": "object",
    "properties": {
        "MeetingTitle": {
            "type": "string"
        },
        "MeetingTime": {
            "type": "string"
        },
        "MeetingOrganizers": {
            "type": "string"
        }
    }
}
```
</details>

### 8. PowerApp または Flow に応答する
Power Apps に Graph API で取得した結果を返却する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2739358/3dd7d40a-002e-fdf1-239b-b8dd7e5446fb.png)

## Power Apps の変更
フローを作成した後でとりあえずのデザインを作成する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2739358/0bc04ee4-cf83-57c2-a49a-13cdaf3cf0fe.png)

OnVisible（画面表示時）に以下を設定。
初期表示時に会議室のメールアドレス設定とメールアドレスを引数に Power Automate を呼び出し、結果を変数に設定。
```
Set(MeetingRoomAddress, "xxxxx@xxxxx.com");
UpdateContext({Reponse: 'PowerAppV2->[作成した Power Automate のフロー名]'.Run(MeetingRoomAddress)});
Set(MeetingTitle, Reponse.meetingtitle);
Set(MeetingTime, Reponse.meetingtime);
Set(MeetingOrganizers, Reponse.meetingorganizers);
```
テキストラベルにはレスポンス値が設定される変数をそれぞれ定義してあげればよい。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2739358/a33c1c82-2b2c-ec0e-3811-55bd8b230517.png)

現時点では画面表示時しか取得できないので例えばタイマーを利用して、数分毎に予約状況を更新すればよい。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2739358/70c5d6e9-b431-315c-41b3-50c12bdafb85.png)

他にも複数の会議室を管理している場合は設定用の画面を設けたり、手動の更新ボタンを用意することで実際の使用に繋げられる。