追記(2021/1/9)

昨年末にSESAME3の販売が開始されました。新機能だけでなく、従来足りていなかった幾つかの機能も追加されています。詳細は下の記事を参照してください。

追加機能の一つとして、QRコードのみでゲストによる鍵の解・施錠機能があります。

残念ながら、本ページで扱っているシステムは従来のSESAMEを想定しているためQRコード機能などには対応していません。しかしながらGASコードについては使える部分も残っていると思いますので、少しでも参考になればと思います。

GASとは

「JavaScriptをベースとしたスクリプト言語を実行できるプラットフォーム」であるらしい、ということは理解しました。

GASのWikiGASのdeveloper用サイトにそれらしいことが書いてある。

Increase the power of your favorite Google apps — like Calendar, Docs, Drive, Gmail, Sheets, and Slides. Apps Script lets you do more with Google, all on a modern JavaScript platform in the cloud. Build solutions to boost your collaboration and productivity.

SESAMEとは

SESAMEの公式サイトをご確認ください。

恐らくご存じの通り、外出先でも鍵の開け閉めや施錠されているか確認できるスマートロックですね。

実はこのSESAME、もともとクラウドファンディングを行って生まれた製品なんですね。

近年では「世界一売れてるスマートロック」とかなんとか言われてるそうです。

このSESAMEですが、専用のスマートフォンアプリから操作を行って施錠・解錠を行うのが一般的な使い方です。

しかし一方では専用のAPI(Application Programming Interface)が用意されており、さらにはIFTTT(webサービスとwebサービスを繋ぐサービス、しかも無料)にも対応しています!これで色々遊べるよ!やったねた〇ちゃん!

めちゃくちゃ色々な機能を追加できるのでこう言うことを楽しめる方にはすごいおススメです!

参考↓

システムで実現したいこと

  1. Google Formから予約日時などの情報を入力し、その内容をSpreadSheetに保存
  2. 予約内容の変更・取り消しが可能
  3. 予約日時に予約者が鍵の解・施錠できる

Google Form + SpreadSheet + Google Calenderで予約システムの構築

まずは上記の組み合わせで予約システムを構築するのですが、実はいい感じにまとめて下さっている先駆者がいました。助かります。

簡単では無い気がするんですが…

一応、上記のサイトを参考にすれば予約システムの根幹部分をほとんど作れます。

(注)Google Caldenderの設定画面のUIなどがアップデートされています。記載された手続きだけでは作れないかもしれません。

SpreadSheetに保存された内容を書き換えて予約の変更・削除を行う

予約の変更・削除についてはいい感じにまとめてあるサイトが無かったので自分なりにコードを書いてみました。

仕様としては以下の通りです。

  1. フォームで予約番号、メールアドレス、予約の削除希望かどうか、希望変更日時を記入してもらう
  2. (予約変更または削除の場合)変更・削除の解答内容を取得
  3. 予約時に発行した予約番号で予約者本人か特定
  4. 予約削除の場合は予約者のメールアドレスに予約削除のお知らせをする
  5. 変更の場合は変更後に先約がないかどうかを確認する
  6. スプレッドシートに変更 or 削除のログを残す…etc.
  7. try{}catch(){}で、システムエラーが出た際にエラーを通知する
function onFormSubmit(e) {
  try{
    //グーグルフォームの回答を取得
    var form = FormApp.getActiveForm();
    var formResponses = form.getResponses();
    var formResponse = formResponses[formResponses.length - 1];
    var itemResponses = formResponse.getItemResponses();

    //回答から予約番号を取得
    var reservation_num = itemResponses[0].getResponse();

    //予約の削除を行うかどうかの解答
    var deleteRes = itemResponses[1].getResponse();

    //回答からメールアドレスを取得
    var mail = e.response.getRespondentEmail();

    //部屋予約フォームのスプレットシート(SS)を開く
    var spreadsheet = SpreadsheetApp.openById('スプレッドシートのID');
    var sheet = spreadsheet.getSheetByName('該当するスプレッドシートのシート名');

    var now_num = 0;
    //SS上で該当する予約番号の行番号(now_num)を取得
    for (var i = 1;i <= sheet.getLastRow();i++){
      if(reservation_num == sheet.getRange(i, 8).getValue()){
        now_num = i;
      }
    }

    //申込者の名前を取得
    var nname = sheet.getRange(now_num, 2).getValue();

    //該当する予約番号が無い場合はメール送信して終了
    if(now_num == 0){
      var thing = nname + "様\n\n 予約がないよ~的なメッセージ";
      MailApp.sendEmail(mail, "ご予約できませんでした", thing);
      return;
    }

    //予約を記載するカレンダーを取得
    var cals = CalendarApp.getCalendarById("googleカレンダーのID的な奴");

    //SSから変更前の開始時間を取得
    var stime = new Date(sheet.getRange(now_num, 4).getValue());

    //SSから変更前の予約の終了時間を取得
    var etime = new Date(sheet.getRange(now_num, 5).getValue());

    var ndate = new Date(sheet.getRange(now_num, 3).getValue());

    //変更前のdate型変数の開始時間(ndates)と終了時間(ndatee)を取得
    var ndates= new Date(ndate.getFullYear(),ndate.getMonth(),ndate.getDate(),stime.getHours(),stime.getMinutes(),0);
    var ndatee= new Date(ndate.getFullYear(),ndate.getMonth(),ndate.getDate(),etime.getHours(),etime.getMinutes(),0);

    //now_numが存在し、ユーザーの予約削除を行いたい場合
    if(deleteRes == "はい"){
      //カレンダーから削除するイベントを取得
      var events = cals.getEvents(ndates, ndatee);
      events[0].deleteEvent();
      sheet.deleteRows(now_num);
      var thing = nname + "様\n\n〇〇時~〇〇時までの予定を削除したよ~的なメッセージ";
      MailApp.sendEmail(mail, "ご予約の削除を行いました", thing);
      return;
    }

    //回答から変更したい予約日を取得
    var pdate = new Date(itemResponses[2].getResponse());

    //回答から変更したい開始時間を取得(itemResponses[3].getResponse()では○○:○○という文字列しか持ってこれないので注意。petimeについても同様)
    var pstime = new Date(pdate.getFullYear(),pdate.getMonth(),pdate.getDate(),itemResponses[3].getResponse().slice(0,2), itemResponses[3].getResponse().slice(-2));

    //回答から変更したい終了時間を取得
    var petime = new Date(pdate.getFullYear(),pdate.getMonth(),pdate.getDate(),itemResponses[4].getResponse().slice(0,2), itemResponses[4].getResponse().slice(-2));

    var pdates= new Date(pdate.getFullYear(),pdate.getMonth(),pdate.getDate(),pstime.getHours(),pstime.getMinutes(),0);
    var pdatee= new Date(pdate.getFullYear(),pdate.getMonth(),pdate.getDate(),petime.getHours(),petime.getMinutes(),0);
    Logger.log("予約日は"+pdate);

    //変更前後の自分の予定が重なっていた場合、無条件で書き換え
    //ndates | pdates | pdatee | ndateeの場合
    //pdates | ndates | ndatee | pdateeの場合
    //pdates | ndates | pdatee | ndateeの場合
    //ndates | pdates | ndatee | pdateeの場合
    if((Moment.moment(ndates).isBefore(pdates) || Moment.moment(ndates).isSame(pdates)) && (Moment.moment(pdatee).isBefore(ndatee) || Moment.moment(pdatee).isSame(ndatee))){
      //予約を行う部分を外部関数化
      reserve(sheet, now_num, pdate, pstime, petime, pdatee, nname, itemResponses, mail, cals);
      return;
    }
    else if((Moment.moment(pdates).isBefore(ndates) || Moment.moment(pdates).isSame(ndates) ) && (Moment.moment(ndatee).isBefore(pdatee) || Moment.moment(ndatee).isSame(pdatee))){
      reserve(sheet, now_num, pdate, pstime, petime, pdatee, nname, itemResponses, mail, cals);
      return;
    }
    else if((Moment.moment(pdates).isBefore(ndates) || Moment.moment(pdates).isSame(ndates)) && (Moment.moment(ndates).isBefore(pdatee) || Moment.moment(ndates).isSame(pdatee))){
      reserve(sheet, now_num, pdate, pstime, petime, pdatee, nname, itemResponses, mail, cals);
      return;
    }
    else if((Moment.moment(ndates).isBefore(pdates) || Moment.moment(ndates).isSame(pdates))&& (Moment.moment(pdates).isBefore(ndatee) || Moment.moment(pdates).isSame(ndatee))){
      reserve(sheet, now_num, pdate, pstime, petime, pdatee, nname, itemResponses, mail, cals);
      return;
    }
    //カレンダーから先約があるかどうか調べ、無ければ書き込む
    else if(cals.getEvents(pdates, pdatee)==0){
      reserve(sheet, now_num, pdate, pstime, petime, pdatee, nname, itemResponses, mail, cals);
      return;
    }
    //他人の先約がある場合は書き変えない
    else{
      var thing =nname+"様 \n\n 先約があって予約できませ~ん的なメッセージ";
      MailApp.sendEmail(mail,"ご予約できませんでした",thing);
      //SSにログを残す
      sheet.getRange(now_num, 9).setValue("予約変更エラー(先約)");
      sheet.getRange(sheet.getLastRow(), 10).setValue("予約変更エラー(先約)");
      return;
    }
  }catch(exp){
    //実行に失敗した時に通知
    MailApp.sendEmail("管理者のメールアドレス", exp.message, exp.message);
    Logger.log("Email wasn't sent");
    var spreadsheet = SpreadsheetApp.openById('SSのID');
    var sheet = spreadsheet.getSheetByName('SSのシート名');
    //予約ログに状態を記入
    sheet.getRange(sheet.getLastRow(), 10).setValue("システムエラー");
    return;
  }
}

function reserve(sh, rnum, pD, psT, peT, pDe, guest_name, GFAns, mail, c){
  //SSから変更後の日時を取得
  sh.getRange(rnum, 3).setValue(pD);
  sh.getRange(rnum, 4).setValue(psT);
  sh.getRange(rnum, 5).setValue(peT);
  //SSに予約状況を記入
  sh.getRange(rnum, 9).setValue("予約変更済");
  //SSにログを残す
  sh.getRange(sheet.getLastRow(), 10).setValue("予約変更済");
  var thing = guest_name+"様 ご予約(変更済み)";
  var r = c.createEvent(thing, pD, pDe);
  var thing = guest_name+"様 \n\n予約の変更を承りました~的なメッセージ";
  MailApp.sendEmail(mail,"ご予約の変更を完了しました", thing);
  return;
}

上記について一つ注意ですが、スプレッドシートにログを必ずしも残す必要はありません。

自分の場合は別件でログを確認する必要があったので記しているだけです。

Google Form(GF)の回答内容からSESAMEの解・施錠を行う

そもそも「GFでSESAMEを操作することに何の意味があるんだ!?」と思われるかもしれませんが、ゲストがSESAMEの操作を行うためには、管理者が手動でゲスト登録と予約日時の設定を行う必要があります。

つまり、「せっかくフォームを通じて自動で予約を行ったにも関わらず、管理人がゲスト登録等を行う際に人為的なミスが生じてしまう可能性がある」ということですね。

ここまでくると最後まで自動で解・施錠を行えるシステムにしたかったという本音もありますが

ちなみにGoogle Formで「lock」、「unlock」を回答することでSESAMEの解・施錠を行うという取り組みには既に先駆者がいました。圧倒的感謝です…!

基本的に上記ページの手順で作れますが、自分はもうひと手間加えて「予約時間内でなければ解・施錠を行えない」という仕組みにしました。

function startSesame(e) {
  try{
    //フォームの解答を取得
    var form = FormApp.getActiveForm();
    var formResponses = form.getResponses();
    var formResponse = formResponses[formResponses.length - 1];
    var itemResponses = formResponse.getItemResponses();

    //開錠情報を取得
    var command = itemResponses[0].getResponse();
    //デバイスIDでSESAMEを識別
    var url = "https://api.candyhouse.co/public/sesame/デバイスID";
    var options = {
      "method" : "POST",
      "headers" : {"Content-Type" : "application/json", "Authorization" : "APIキー"},
      "payload" : JSON.stringify({"command" : command})
    };

    //予約番号をフォーム解答内容から取得
    var reservationNumber = itemResponses[1].getResponse();
    Logger.log('予約番号は'+reservationNumber)

    //部屋予約フォームのスプレットシート(SS)を開く
    var spreadsheet = SpreadsheetApp.openById('SSのID');
    var sheet = spreadsheet.getSheetByName('SSのシート名');

    //予約番号(now_num)が記述されている行(スプレットシート内)
    var now_num = 0;
    //該当する予約番号の行番号(now_num)を取得
    for (var i = 1;i <= sheet.getLastRow();i++){
      if(reservationNumber == sheet.getRange(i, 8).getValue()){
        now_num = i;
        break;
      }
    }

    //開錠予約日の取得
    var ndate = new Date(sheet.getRange(now_num, 3).getValue());

    //開錠予約日の開始時間を取得
    var stime = new Date(sheet.getRange(now_num, 4).getValue());
    var ndates= new Date(ndate.getFullYear(),ndate.getMonth(),ndate.getDate(),stime.getHours(),stime.getMinutes(),0);

    //開錠予約日の終了時間を取得
    var etime = new Date(sheet.getRange(now_num, 7).getValue());

    //現在時刻を取得
    var today = new Date();
    var todayHour = Utilities.formatDate(today, 'Asia/Tokyo', 'HH');
    var todayMinute = Utilities.formatDate(today, 'Asia/Tokyo', 'mm');

    //開錠時間内の操作のみ許す
    if(ndates.getFullYear() == today.getFullYear() && ndates.getMonth() == today.getMonth() && ndates.getDate() == today.getDate() && stime.getHours()*3600+stime.getMinutes()*60todayHour*3600+todayMinute*60){
      var response = UrlFetchApp.fetch(url, options);
  }
  catch(exp){
    //実行に失敗した時に通知
    MailApp.sendEmail("管理者のメールアドレス", "SESAMEの解・施錠に失敗しました", "エラー内容は以下の通りです\n\n"+exp.message);
    Logger.log(exp.message);
    Logger.log("Email wasn't sent");
  }
}

何も難しいことは何にもしてません。単にif文分岐してるだけですね。

Qiitaの記事にも書いてありますが、この状態では予約番号のみでの1段階認証しかできていません。運用には注意してください。

まとめ

SESAMEの専用スマートフォンアプリなどのUIに不慣れな方や、そもそもスマートフォンをお持ちでない方に鍵の管理を任せる際には、こうしたGoogle FormとGoogle Calenderのみの仕組みを使ってもらう方がより安全かもしれません。

今回、GASを初めて + しかもそれなりの量を書いてみたのですが、マニュアルやヘルプが豊富で(ほとんど英語でしたが)こういった予約システムに限らず様々なサービスを自作できそうですね。

自分の本業はwebアプリではないので他のサービスを作るモチベはあまり無いですが、機会があれば他にも作ってみたいですね。バイトさせてくれる企業様募集中です。

【追記:2020-9-8】今ならもっとましなスクリプトを書けると思うんじゃ