M5StackをアクションボタンにしてPower Automate経由でTeamsにコーヒーできたよ通知を出す

## はじめに

こんにちは。(株)東芝 研究開発センターの向本と申します。

業務では無線センサネットワークに関する研究開発を行っています。

最近業務で[M5Stack](https://www.switch-science.com/collections/m5stack?srsltid=AfmBOorx8912C_vAx4mPrehkhuINchcWoDXfkZgwKbgYY_3fLWIX2B_v)というデバイスを使っています。
これはWi-FiやBluetoothといった無線機能やディスプレイ、ボタンなどが標準搭載されており、IoT機器の試作を手頃に行える点が非常に魅力的です。

今回はこれを使って職場にアクションボタンを置くことを考えてみました。

今年新設された東芝の研究開発拠点、[イノベーション・パレット](https://www.global.toshiba/jp/technology/corporate/rdc/newbuilding/working.html)では、職場のフリーアドレス化やリモートオフィスが推進されています。
これにより柔軟な働き方ができるようになった一方で、今までにはなかったようなちょっとした困りごとが出てくるようになりました。
例えば、職場では共用のコーヒーメーカーを置いて従業員が各自でコーヒーを淹れているのですが、今まではコーヒーができた際に同僚も近くで仕事をしているのでそれに気づけたのですが、今の働き方だと離れた場所にいて気づかないことがしばしばあります。
そういった際に同僚に直接声を掛けて回るのは手間なので、Microsoft Teams等のコミュニケーションツールを使って連絡することになります。
しかし、この時自席に戻ってわざわざメッセージを送るのも少々面倒です。

そこで、M5Stackでアクションボタンを作り、ボタンを押すだけで気軽にTeamsでコーヒーを淹れたことを連絡できるシステムを作りました。

イメージは以下の通りです。コーヒーメーカーの近くにM5Stackを置いておき、コーヒーができたことを見つけた誰かがコーヒーを取りに来たついでにボタンを押してみんなに知らせるという形です。

![コーヒーできたよボタンの利用例](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940014/fa2e51ae-9d9e-2040-929d-da34230a4d88.jpeg)

## アクションボタン構成

構成は以下の通りです。

![アクションボタンのシステム構成](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940014/78cba766-5e9a-d604-952a-200cc116fec0.png)

M5Stackはコーヒーメーカーの近くに置いておき、誰かがコーヒーができたことを見つけたらM5Stackのいずれかのボタンを押し、Teamsへ通知が飛ぶという動作を想定しています。
Teamsへの自動投稿にはMicrosoft Power Automateを使用します。M5StackからPower Automateへの通知にはメールを利用します。

### Microsoft Power Automate

外部サービスをトリガとしてTeamsにテキストチャットを投稿する手頃な手段として、Microsoft Power Automateがあります。こちらはOffice365に含まれるサービスになります。

Power Automateは1つのトリガと複数のアクションを接続することで動作します。
ここではトリガとアクションの総称をノードと呼ぶことにします（公式になんと呼ばれてるのかがわからず…）。
この時、ノードによってはさらに詳細な設定を行う必要があります。

今回はメールの受信とトリガ、Teams投稿をアクションとするので、接続形態は以下のようになります。

![アクションボタンのPower Automateフロー](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940014/7feebff5-b134-32ec-d3b7-4aa378babfbb.png)

まずはアクションの方を設定します。
今回の場合、Power AutomateでやりたいことはTeamsへの投稿なので、アクションとして「チャット、またはチャネルでメッセージを投稿」ノードを使用します。
このノードでは投稿先のチャネルを設定する必要があります。

設定は主に「パラメータ」タブから行います。
今回の場合は投稿者、投稿先、チーム、チャネル、メッセージの5種類を設定します。
投稿者はフローボットもしくはユーザから選択します。
ここは好みでよいと思いますが、フローボットを選択した場合は投稿の編集や削除ができない点は留意ください。
次に投稿先、チーム、チャネルでメッセージの投稿先を決定します。
それぞれプルダウン形式で自身が所属するグループチャット、チャネルから選択できるので、迷うことはないと思います。
メッセージには投稿する内容を記載します。
以上を埋めると以下のような形になります。

![Teams投稿アクションの設定画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940014/881cb0f5-d655-3c02-8042-c3175515dc82.png)

次にトリガの設定を行います。
トリガに使うノードにはいくつか候補がありますが、最も手頃なものがHTTP リクエストになります。
これはHTTPアクションの1つで、Power Automateで生成されたURLに対して何らかのデバイスからHTTPリクエストを送信することでトリガを起動させることができます。
M5StackでHTTPリクエストを送信することは容易で、UIFlowのようなローコード開発環境を用いた場合でも用意に実装できます。
しかし、HTTPに関するアクションは特別な有償プランに含まれるため、場合によっては利用できないことがあります（実際弊社では扱えませんでした……）。
そこで今回は、メールを利用してトリガを起動する方法を用います。

メールの受信を用いたトリガでは、Power AutomateユーザがOutlookに登録しているメールアドレスで受信したメールの内容によってアクションを仕掛けることができます。
このノードでは最大9種類のパラメータを設定することができ、これによりトリガとなるメールの条件を決められます。
設定項目は下図の通りで、送受信者のアドレスや、メールの件名、トリッキーなところだとメールが振り分けられる受信トレイ名というものもあります。
今回は送信するメールの内容を自由に決められるため、単純なものとして送信者と件名の組み合わせをトリガとします。
上記項目を設定すると以下のようになります。

![メール受信トリガの設定画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940014/d00dd074-d99f-76dd-5652-88018e48ed4b.png)

これでPower Automateの設定は完了です。
画面上のデプロイを押すと上記フローが起動します。

### M5Stack実装

次に現地に置くM5Stackのファームウェア実装を行います。
M5Stackにも色々な種類がありますが、今回はM5Stack Core2 for AWSを用います。
M5Stackのファームウェア開発はArduinoやUIFlowなど様々なプラットフォームで行うことができますが、今回はESP-IDFで開発しようと思います。
ESP-IDFはM5デバイスに搭載されているマイコン、ESP32の製造元であるEspressif Systems社が提供する開発環境で、他の開発環境と比べると環境構築のハードルが少し高いものの、細かな制御まで行えるため複雑な動作をさせる場合にお勧めとなります。
今回はそこまで複雑な動作は行わないので、Arduinoでも同じ動作はさせられると思います。
ESP-IDFを使った開発を行う場合、[PlatformIO](https://platformio.org/)を利用することがオススメです。
PlatformIOはマイコン開発環境の一つで、ESP32を公式にサポートしておりtoolchainやSDKなど環境構築に必要なものを自動で取得してくれるため簡単に開発環境を整えることができます。
VS CodeのExtensionから導入できる点も魅力的です。

ESP-IDFでメール送信を行う場合、[ESP-Mail-Client](https://github.com/mobizt/ESP-Mail-Client)というライブラリを使用します。
これは第三者が公開しているOSSで、PlatformIOを利用する場合はライブラリ検索機能から導入することができます。

最初にメール送信およびWi-Fi接続に必要な情報を記載します。
SSIDとPASSWORDはWi-Fi接続用、そのほかの情報はメール送信用の情報になります。
また、今回は社内ネットワークに実験用に立てているメールサーバを送信元とします。
こちらはユーザ名のみでアクセスできるため、パスワードは不要でポート番号も25番となりますが、Gmailを使う場合など状況によってはパスワードが必要だったり別のポート番号を使う場合があります（場合によってはプロキシ設定も必要になりそうですね）。

~~~c

const char SSID[] = "ssid name";
const char PASSWORD[] = "Password"; // Wi-Fiのパスワード
const char MAIL_ADDRESS[] = "sender mail address"; // 送信元のメールアドレス
// const char MAIL_PASSWORD[] = "yourmailPassword"; // 送信元のメールサーバのパスワード（今回は不要）
const char TO_ADDRESS[] = "receiver mail address"; // 送信先のメールアドレス
const char MAIL_SUBJECT[] = "Coffee test"; // メールの件名

const char MAIL_BODY[] = "It's a coffee time"; // メールの本文（今回は何でもいいです）

const char SERVER_NAME[] = "smtp server host"; // SMTPサーバーのホスト名
const int SERVER_PORT = 25; // SMTPサーバーへの接続に使うポート番号
const char SENDER_NAME[] = "sender name"; // SMTPサーバログイン時のユーザ名

~~~

次にメール送信に関する処理send_email()および送信処理のコールバックsmtpCallback()を記載します。

callbackにはメール送信の結果を取得した際のふるまいを記述します。
今回はメールの送信後に行う処理はないので、デバッグ用として単純に結果をシリアル線に表示する処理のみを行っています。

send_email()ではメールサーバへのアクセスやメールの内容に関する設定を行います。
前述したように今回は特に認証処理を行わないメールサーバを使いますので、サーバへのログインにかかる設定は空欄としています。
また、サーバとの通信のために正確な時刻を取得する必要があるため、NTPサーバにアクセスして時刻情報を取得します。
M5stackにはRTCが搭載されているので、実際にはこの処理を何度も行う必要はありません（今回は簡単化のために毎回アクセスしています）。

~~~c

void smtpCallback(SMTP_Status status)
{
  Serial.println(status.info());
}

void send_email(void) {
  config.server.host_name = SERVER_NAME;
  config.server.port = SERVER_PORT;
  config.login.email = ""; // SMTP Authenticationがない場合は空欄
  config.login.password = ""; // 同上
  
  config.login.user_domain = ""; // 同上

  config.time.ntp_server = "ntp server ip address";
  SMTP_Message message;

  // Set the message headers
  message.sender.name = SENDER_NAME;
  message.sender.email = MAIL_ADDRESS;
  message.subject = MAIL_SUBJECT;
  message.addRecipient(SENDER_NAME, TO_ADDRESS);

  // Set the message content
  message.text.content = MAIL_BODY;

  // Set debug option
  smtp.debug(1);

  // Set the callback function to get the sending results
  smtp.callback(smtpCallback);

  // Connect to the server
  smtp.connect(&config);

  // Start sending Email and close the session
  if (!MailClient.sendMail(&smtp, &message)){
    M5.Lcd.println("Error sending Email, " + smtp.errorReason());
    delay(10000);
  } else{
    M5.Lcd.println("Thank You for making coffee!\n");
    delay(5000);

  }

}

~~~

最後にmain関数でボタンが押された場合にsend_email()を呼び出すように記載して完成になります。
以上の設定を行うことで、M5Stackのボタンを押すとメールが送信され、このメールの受信をトリガとしてTeamsに投稿することができます。

## おわりに

実際に使ってみると以下のようになります。
コーヒースペースに置かれたM5Stackのボタンを押すとこのような表示になり、Teamsでは以下のような通知が来ます。

![ボタンを押した際のM5Stackの表示](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940014/d9d4f8a8-b364-3f41-589c-85ccd43a14a4.png)

![Teamsの自動投稿画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940014/d5ea19d9-0df5-067b-5b79-649a67869588.png)

最後にM5Stackのプログラム全体を記載します。
上記説明以外の部分としてWi-Fi接続や画面表示に関する記述もありますが、基本的な挙動かつ本筋ではないためここでは説明を割愛します。

今回はコーヒーができたことを連絡する目的でアクションボタンを作りましたが、会社に荷物が届いたときなど他の連絡にも使うことができます。
また、アクションの内容を変更することでこういった通知以外にも様々なものが作れると思いますので、是非お試しください。

~~~c
#include <M5Core2.h>
#include <WiFi.h>
#include <ESP_Mail_Client.h>

const char SSID[] = "ssid name";
const char PASSWORD[] = "Password"; // Wi-Fiのパスワード
const char MAIL_ADDRESS[] = "sender mail address"; // 送信元のメールアドレス
// const char MAIL_PASSWORD[] = "yourmailPassword"; // 送信元のmailのパスワード（今回は不要）
const char TO_ADDRESS[] = "receiver mail address"; // 送信先のメールアドレス
const char MAIL_SUBJECT[] = "Coffee test"; // メールの件名

const char MAIL_BODY[] = "It's coffee time"; // メールの本文（今回は何でもいいです）

const char SERVER_NAME[] = "smtp server hostname"; // SMTPサーバーのホスト名
const int SERVER_PORT = 25; // SMTPサーバーへの接続に使うポート番号
const char SENDER_NAME[] = "sender name"; // SMTPサーバログイン時のユーザ名

// Declare the global used SMTPSession object for SMTP transport
SMTPSession smtp;

// Declare the global used Session_Config for user defined session credentials
Session_Config config;

// Callback function to get the Email sending status
void smtpCallback(SMTP_Status status);

void setup_display(void) {
    M5.Lcd.fillScreen(BLACK);  // Set the screen background.
    delay(500);                // Delay 500ms.

    M5.Lcd.setCursor(10, 10);  // Move the cursor position to (x,y).
    M5.Lcd.setTextColor(WHITE);  // Set the font color to white. 
    M5.Lcd.setTextSize(2);  // Set the font size. 
    M5.Lcd.printf("Hello!");  // Serial output format string. 

    // wait drawing
    delay(1000);

    return;
}

void setup_wifi(void) {
    WiFi.begin(SSID, PASSWORD);                         // Wi-Fi接続開始
    while (WiFi.status() != WL_CONNECTED) { 
      delay(500); 
      M5.Lcd.print(WiFi.status());
    } 
  
    M5.Lcd.println("");                               // Wi-Fi接続結果をディスプレイへ出力
    M5.Lcd.println("WiFi connected"); 
    M5.Lcd.println("IP address: "); 
    M5.Lcd.println(WiFi.localIP());                         // IPアドレスをディスプレイへ出力

    return;
}

void smtpCallback(SMTP_Status status)
{
  Serial.println(status.info());
}

void send_email(void) {
  config.server.host_name = SERVER_NAME; // for outlook.com
  config.server.port = SERVER_PORT; // for TLS with STARTTLS or 25 (Plain/TLS with STARTTLS) or 465 (SSL)
  config.login.email = ""; // set to empty for no SMTP Authentication
  config.login.password = ""; // set to empty for no SMTP Authentication
  
  config.login.user_domain = "";

  config.time.ntp_server = "ntp server ip address";
  SMTP_Message message;

  // Set the message headers
  message.sender.name = SENDER_NAME;
  message.sender.email = MAIL_ADDRESS;
  message.subject = MAIL_SUBJECT;
  message.addRecipient(SENDER_NAME, TO_ADDRESS);

  // Set the message content
  message.text.content = MAIL_BODY;

  // Set debug option
  smtp.debug(1);

  // Set the callback function to get the sending results
  smtp.callback(smtpCallback);

  // Connect to the server
  smtp.connect(&config);

  // Start sending Email and close the session
  if (!MailClient.sendMail(&smtp, &message)){
    M5.Lcd.println("Error sending Email, " + smtp.errorReason());
    delay(10000);
  } else{
    M5.Lcd.println("Thank You for making coffee!\n");
    delay(5000);

  }

}

void setup() {
    M5.begin();                // Init M5Core2.
    setup_display();
    setup_wifi();
}

void loop() {

  M5.update();  // 本体のボタン状態更新

  if (M5.BtnA.isPressed()||M5.BtnB.isPressed()||M5.BtnC.isPressed()) {         // ボタンが押されていれば
    send_email();

    M5.Lcd.begin();
    M5.Lcd.setCursor(10, 10);  // Move the cursor position to (x,y).
    M5.Lcd.println("Please press any buttons after making coffee.");
  }

}

~~~

※本記事に掲載の商品、機能等の名称は、それぞれ各社が商標として使用している場合があります。