2021年6月16日水曜日

【SESAME4 / SESAME3】 GAS から Web API を利用するサンプル (Sesame OS2)

2021/10/30 更新

複数台操作の不具合の指摘をうけ、修正がてらプロパティ周りの仕様を大幅変更。


 【まえがき:APIが刷新されていた】


SESAME3用のAPI* がひっそりと上がっていた(正式発表はまだ?)ので、Google Apps Script からいじれるようにしてみた。
*追記:セサミ4を含むSesame OS2の入ったデバイスに対応。

従来のものとは仕様が変更され、導入のハードルがすこし上がった感じ。
GASにこだわらないなら pysesame3 が便利。

今回は、以前書いたセサミミニ用のスクリプトから乗り換えやすいよう、使用方法を極力継承してセサミ3に対応した新しいスクリプトを書いてみた。



【事前準備1:必要な各種データを入手】


1. 公式のユーザーページ(https://partners.candyhouse.co/)にログインし、APIキーを取得。


2. 以下のいずれかを行う。(現在はB推奨)

  A. セサミアプリでマネジャー権限のQRコードを発行し、好みのQRコードリーダーで読み取り、中身のテキスト(ssm://UI? から始まる長い文字列)をコピーしておく。
(以前はSecret Keyなどを簡単に取得できなかったためこのようにするしかなかった。もしBでうまくいかない場合はこちらで。)


  B. ユーザーページの user devicesボタンから任意のデバイスを選択し、UUIDSecret Keyを取得。





 


【事前準備2:外部JavaScriptを追加】


施解錠操作用の署名生成がややこしくて自力では困難なため、artjombさんの cryptojs-extension を拝借する。

1. まずはスクリプトファイルを新規作成
(Googleドライブ 新規→その他→Google Apps Script)

  • lib/cryptojs-aes.min.js
  • build/cmac.min.js
の内容をそれぞれ丸ごとスクリプトファイルに追加(コピペ)する。





ファイル + からスクリプトを追加できる。
※初期状態で表示されるテキストは不要なので、テキストエリアを空白にしてから貼り付け

名前はなんでも良いけれどわかりやすくそのまま。


【汎用サンプルコード】

ようやくメインのコード。

はじめに、「プロジェクトの設定」でChrome V8 ランタイムが有効になっていることを確認。(多分デフォルトでなっている。)

コード.gsに戻り、テキストエリアを空白にしてから以下をコピペ。

const prop = PropertiesService.getScriptProperties();
const myKey = prop.getProperty('myKey');
const apiKey = prop.getProperty('apiKey');

// プロパティ登録 (*****部分を書き換えて初回のみ実行する。実行後は消してもOK)
function prepare(){
  prop.deleteAllProperties();
  prop.setProperty('myKey', '*****'); // 任意のパスキー。自由に設定する
  prop.setProperty('apiKey', '*****'); // ダッシュボードで取得したAPIキー
  
  // 事前準備A組は↓に、QRから取得したテキスト全文を入れる
  analyzeQR('*****');
  //analyzeQR('*****'); // デバイスが複数ある場合、適宜追加

  // 事前準備B組は↓に、セサミに付けた名前, UUID, SecretKey の順で入れる
  analyzeQR('*****', '*****', '*****');
  //analyzeQR('*****', '*****', '*****'); // デバイスが複数ある場合、適宜追加
  
  showProps();
}

//プロパティ確認
function showProps(){
   console.log(prop.getProperties());
}

// 動作テスト。成功したら200が返り、セサミが動く
function test() {
  main(myKey, "セサミの名前", 2, "GAS");
  /*
  説明:
  "セサミの名前" を実際のセサミの名前に変更する
  "GAS" はアプリの通知と履歴に表示される名前。変更可
  
  セサミの名前について補足説明:
  将来もしアプリ上でセサミの名前を変更したとしても、このスクリプトの運用に影響はありません 
  設定を更新する必要はなく、逆に言えば、prepare()実行時点の名前を使い続ける必要があります
  新しい名前をGASでも使いたい場合は、新しい情報で再度prepare()を実行します
  */
}

function doPost(e) {
  const p = JSON.parse(e.postData.contents);
  main(p.myKey, p.deviceName, p.command, p.user);
}

function doGet(e) {
  const p = e.parameter;
  main(p.myKey, p.deviceName, p.command, p.user);
}

// 施解錠操作
function main(key, device, command, user='ウェブアプリ') {
  if(key != myKey) return;
  const c = [83, 82, 88][parseInt(command)]; // lock:82,unlock:83,toggle:88
  const h = Utilities.base64Encode(user, Utilities.Charset.UTF_8);
  const devices = device.split(',');
  devices.forEach(function(name) {
    const data = JSON.parse(prop.getProperty(name));
    const body = {
      'cmd': c,
      'history': h,
      'sign': generateCmacSign(data.secKey)
    }
    const options = {
      headers: {'x-api-key': apiKey},
      method: 'POST',
      muteHttpExceptions: true,
      payload: JSON.stringify(body)
    }
    const url = `https://app.candyhouse.co/api/sesame2/${data.uuid}/cmd`;
    const response = UrlFetchApp.fetch(url, options).getResponseCode();
    console.log(response);
  });
}

// CMAC認証
function generateCmacSign(secKey) {
  const date = Math.floor(Date.now() / 1000);
  const dateDate = new DataView(new ArrayBuffer(4));
  dateDate.setUint32(0, date, true);
  const msg = dateDate.getUint32(0).toString(16).slice(2, 8);  
  const hex = CryptoJS.enc.Hex.parse;
  return CryptoJS.CMAC(hex(secKey), hex(msg)).toString();
}

// QR情報デコード
function analyzeQR(p1, p2, p3){
  if(p1 == '' || p1.indexOf('*') == 0) return;
  let name;
  let data = {};
  if(p2){ // デコード済みデータが来てる
    name = p1;
    data = {'uuid':p2, 'secKey':p3};
  }
  else{ // 生データが来てる
    const ssm = decodeURIComponent(p1)
    const params = ssm.slice(ssm.indexOf('?') + 1).split('&');
    params.forEach(function(p) {
      if (p.indexOf('sk=') == 0){
        const sk = p.slice(3);
        const uuid = `${hx(sk,83,86)}-${hx(sk,87,88)}-${hx(sk,89,90)}-${hx(sk,91,92)}-${hx(sk,93,98)}`;
        data.uuid = uuid.toUpperCase();
        data.secKey = hx(sk, 1, 16);
      }
      else if(p.indexOf('n=') == 0){
        name = p.slice(2);
      }
    });
  }
  prop.setProperty(name, JSON.stringify(data));
}

// QR情報デコード Core
let hx = (data, start, end) => {
  return Utilities.base64Decode(data).slice(start, end + 1)
  .map(function(chr){return (chr+256).toString(16).slice(-2)}).join('');
}



【使用方法】


【1. プロパティを登録】

まず初回のみプロパティを登録する。
  1. function prepare() 内の *****部分を自前の情報に書き換え、保存ボタンを押す。
  2. 関数メニューで「prepare」を選択し、実行ボタンを押す。

【2. 動作チェック】

  1. function test() 内の情報を適宜書き換える。
  2.  関数メニューで「test」を選択し、実行ボタンを押す。

成功したら200が返り、セサミが動く。

うまく行かない場合は…
主な返り値と原因の可能性
404:サーバーダウン OR ルーターやWi-Fiモジュール等の問題かも。
403:APIキーが間違っているかも。
502:UUIDが間違っているかも。
200なのに動作しない:SecretKeyが間違っているかも。


【3. ウェブアプリ化】

  1. 「デプロイ」 → 「新しいデプロイ」
  2. 「種類の選択」で「ウェブアプリ」を選択
  3. 「次のユーザーとして実行」を自分、「アクセスできるユーザー」を全員にする。
  4. デプロイ実行






















途中で「承認が必要」と出た場合は流れに従って承認する。
(ログイン→詳細→安全ではないページに移動→許可)

最後に表示されたウェブアプリURLをコピーしておく。


【4. 運用】

あとは従来同様、このURLに適切にリクエストを送れば良い。
より汎用的にするためPOST、GET両方用意しているけれど、基本的にはPOSTでOK。

パラメータは従来からひとつ増え、myKeydeviceNamecommanduserの4つ。
  • myKey:prepareで準備した任意のパスキー。一致しないとはじかれる。

  • deviceName:セサミの名前。複数台同時に動かしたい場合は、カンマ区切りで「玄関,玄関2,倉庫」のように。

  • command:数字で、0~2のいずれか
    0=解錠 1=施錠 2=トグル
    (新しいAPIにはトグル機能が標準で備えられているため、従来のコマンド2および3は2に集約された。)

  • user:通知や履歴に表示される名前。省略可。省略した場合「ウェブアプリ」となる。
    ※userに特定の文字列を指定すると一部が文字化けする事象を確認済み。特定の文字というわけでなく、規則性をつかめず。現時点で詳細不明。



【最後に:スクリプトをあとから編集した場合】

スクリプトの編集をウェブアプリに反映させるには再デプロイが必要。

1. 編集を保存
2. 「デプロイ」→「デプロイを管理」
3. 編集ボタン(右上のペンアイコン)を押して、「バージョン」から新バージョンを選択
4. デプロイ実行


以上。

(注意・免責)リスクを管理し、あくまで自己責任で使用してください。

【余談】

今回は施解錠のみのスクリプトのため以上となるけれど、新APIはGETも刷新されていて、履歴やバッテリ電圧、サムターン角度なども取得できるようになっていた。アイデア次第で便利に使えそうだ。


42 件のコメント:

  1. "ReferenceError: CryptoJS is not defined"になります。

    返信削除
    返信
    1. 別の方からは問題なく使用できた報告を受けているので、おそらく外部JavaScriptの追加方法が間違っています。

      削除
  2. コメント失礼します。
    非常に有益な情報をありがとうございます。
    参考にさせていただいております。
    初学者のためぜひアドバイスいただきたく存じます。
    GASの設定までは問題なく行えたのですが、POST送信がうまく機能せず困惑しております。
    下記内容をwebhookでPOSTしているのですが、不備があればご教授いただきたく存じます。

    POST 生成されたウェブアプリURL
    Content Type application/json
    Body
    [{
    "myKey": "任意の数字",
    "deviceName": "セサミ", //アプリでセサミに名称変更済のため
    "command": "0",
    "user":"ウェブアプリ"}
    ]

    返信削除
    返信
    1. ご使用の環境がわからないため断定はできませんが、通常Body全体を角括弧で囲むことはしないと思いますので、一度 [] を外してみてください。

      例として、IFTTTのアクションでWebhookを使用する場合、Bodyの入力欄は以下のようにします。

      {"myKey":"*****","deviceName":"セサミ","command":"0","user":"ウェブアプリ"}

      削除
    2. お返事ありがとうございます。
      無事に作動確認できました、ご教授ありがとうございました。

      削除
  3. 閉開の履歴を取得してwebhookを使い玄関の電気のオンオフを考えています。
    セサミオープンで電気がついて
    鍵を締めたら自動で電気オフができたら便利かなと思いました。
    公式に確認したら直接のwebhookは開発中で年内にはとの回答でした。
    お手好きにでもgasで履歴を取得してwebhookなどの記事を書いて頂けると助かります。

    返信削除
    返信
    1. 現状、GASで公式webhookの動作を代替することは不可能と思われます。
      履歴取得は可能なのですが、高頻度で叩くとすぐ429 Too Many Requestsエラーになるため、履歴をもとに即時に何かさせるのは現実的ではありません。

      また公式webhookですが、旧版では手動開閉時にも発動する仕様でしたので、新版も同様であれば、「施錠で消灯」というルールでは帰宅時手動施錠した際、玄関にいる状態で真っ暗になってしまうのではと想像します。

      もし、外出時の施錠と帰宅時の解錠のみをAPI(GAS経由)で行う予定であれば、GASで同時に照明も操作してしまうのが確実だと思います。

      もしくはIoTではないですが、人感センサー付き電球も環境次第でアリだと思いますよ。うちは廊下や玄関はそうしています。

      削除
    2. そうでしたかすいませんでした。
      閉めた際に電気がオフになるのはオートロックにしているので玄関にいない時になるので大丈夫です。

      Gasってなんかしらのアプリから開け締めできますでしょうか

      人感センサーは実家で使ってますが玄関直の狭いワンルームなのでちょくちょくついてしまいます。

      削除
    3. いえいえ。
      なるほど、そういうことですね。

      GASの実行はHTTPリクエストを送信できるアプリから行えます。iPhoneなら標準の「ショートカット」アプリ、Androidなら「Tasker」など多数ありますので詳しくは調べてみてください。

      ただ、オートロックはGASではリアルタイム検知できないので、別案が必要です。パッと思いつくのは、帰宅時一発実行で「解錠→点灯→待機→消灯」をするプログラムでしょうか。これならGASで可能だと思います。

      削除
    4. そうですよね
      年内のwebhookを期待して待ちます。
      ありがとうございます

      削除
  4. 質問失礼します
    Sesame4で2台同時にIFTTTで解錠しようとしています
    1台であれば動作しますが、2台にするとSKを入れた方のSesameのみ動作します
    それぞれSKが異なるようですが、2台同時に動作させる方法をご教授願います
    bodyは、{"myKey":"*****","deviceName":"セサミ,セサミ2","command":"0","user":"ウェブアプリ"}としています

    返信削除
  5. 質問失礼します
    Sesame4で2台同時に動作させようとしています
    色々とトライしましたが1台のみであれば動作しますが、2台にするとSKを入れたSesameのみ動作します
    SKは一つ一つ異なると思いますが、同時動作させる方法がありましたらご教授願います
    Bodyは{"myKey":"*****","deviceName":"セサミ,セサミ2","command":"0","user":"ウェブアプリ"}としています
    お手数ですがご確認お願いします

    返信削除
    返信
    1. 貴重な情報ありがとうございます。これは設計ミスですね。
      一台しか所有していないのに公開にあたり汎用仕様を求めたせいで、複数台の動作検証ができていませんでした。

      skをデバイスごとにプロパティに保存して呼び出すよう書き換えれば動作すると思います。近いうちに修正更新します。
      ご指摘ありがとうございました。

      削除
    2. 返答ありがとうございます
      修正お待ちしています

      削除
    3. 修正しました。お手数ですがコードのコピペから再度試してみてください。

      当該箇所を修正し、データ準備の方法を全体的に見直しています。
      それに伴い、事前準備1の項も加筆修正しました。確認をお願いします。

      もしまだ問題があればご報告ください。

      削除
    4. 早々のご対応ありがとうございました
      問題なく2台でも動作しております

      削除
  6. ブログを拝見し、見よう見まねで挑戦したところ、【1. プロパティを登録】のところでエラーが発生し進めません。
    「****」の箇所はすべて入力しているつもりですが、事前準備A組、B組に不備があるのでしょうか?
    お時間がありましたら、ご支援のほどよろしくお願いいたします。


    ----以下エラー内容----
    エラー
    Exception: Could not decode string.
    hx @ function.gs:114
    (匿名) @ function.gs:101
    analyzeQR @ function.gs:98
    prepare @ function.gs:12

    返信削除
    返信
    1. 自前のQRでは成功してしまうので、原因究明にご協力ください。
      一部の値に対してのバグかもしれません。

      まず、テキストが ssm://UI?~~~&sk=*****&~~~ の構造になっているか確認してください。&sk=から次の&までの*で示した箇所の値が重要です。
      この箇所に英数字以外の記号などが含まれていれば、その文字を教えてください。ヒントになるかもしれません。よろしくおねがいします。

      削除
  7. ご返事、ありがとうございます。

    バグかもとご心配をおかけしましたが、見直したところssm://UI? から始まる長い文字列に誤りがあり、修正したところエラーは無くなりました。

    大変申し訳ございませんでした。

    返信削除
  8. 正に求めていた情報で非常に感激いたしました。ありがとうございます。

    SESAME3をウェブ上で操作出来るまでには至りましたが、目的であるwena3からRiiiverを使ってGASを実行することがうまくいきません。

    デプロイID:生成されたものをコピー
    パラメータ:{"myKey":"*****","deviceName":"セサミ","command":"0","user":"ウェブアプリ"}

    上記のようにしているのですが、考えられる不備等ありますでしょうか?
    宜しければお手漉きの際にでもお教えいただけないでしょうか?

    返信削除
    返信
    1. デプロイIDではなくウェブアプリURLをコピーして使用します。
      パラメータの方は問題ないかと思います。

      削除
    2. 早速のご返信ありがとうございます。

      入力箇所には
      URL:https://script.google.com/macros/s/XXX/exec?YYY
      【デプロイID(XXX)】
      【パラメータ(YYY)】
      と記載があります。

      また、デプロイID入力箇所にウェブアプリURLを入力してみても動作しませんでした。

      削除
    3. そうなんですね。通常HTMLリクエストではデプロイIDの入力は不要なので、その仕様はちょっとわかりかねます。もしかすると実行可能APIとしてデプロイする方法かもしれませんが、詳しくありません。(Google Cloud Platformへの決済情報登録が必要だった気がします)

      一応関連すると思われる記事を見つけました。これ以上のことはわかりません。
      https://jellyware.jp/riiiver/contents/contents20.html

      Riiverについて少し調べてみましたが、IFTTT Webhookも使えるようなので、もし経路にこだわりがなければIFTTT経由を試してみてください。
      「Webhookトリガー → Webhookアクション」でHTMLリクエストが使用できると思います。

      削除
  9. そうですか…。
    迅速なご返信ありがとうございました。
    あきらめることにします。

    返信削除
    返信
    1. 自身で色々試したところ、解決致しました。
      パラメータに問題がありました。
      myKey=*****&deviceName=セサミ&command=0&user=ウェブアプリ
      としたことで動作しました。
      お騒がせしました。

      削除
    2. なるほどGETだったんですね。良かったです。
      その書き方はPOSTではなくGETメソッドです。前提がPOSTの話と思っていてパラメータは問題ないとお答えしてしまいました。
      無事動作して良かったです。

      削除
    3. doPOSTとdoGETの違いの理解が浅いのですが、GETに対応していただいていたおかげかと思います。本当にありがとうございます。

      あとはriiiverのGASの項目が【GAS実行】と【GAS GETメッセージ】がありまして、【GAS実行】ではどのようにしても無理でした。
      【GAS GETメッセージ】で上記設定で動作しましたので、今後同じ目的の方がおられましたら参考にしていただけたらと思います。

      本当にありがとうございました。

      削除
    4. 貴重な情報ありがとうございます。
      私もWena3とSesame3を使っていて、このサイトの情報を元に設定したのですが、うまく動かず困っておりもし何かわかるようでしたら教えてもらえませんでしょうか?

      ・ブラウザで下記のURLを実行すると解錠されることまでは確認しています(GASの設定までは問題なくできていると思われます)
      https://script.google.com/macros/s/XXXXX/exec?myKey=*****&deviceName=セサミ&command=0&user=ウェブアプリ

      ・Wena3のRiiiver設定にて、【GAS GETメッセージ】を使って、Serviceとして、以下の設定をしました
       デプロイID: XXXXX(上記URL内と同じ文字列)
       パラメータ: mykey・・・(上記URLのmykey以降の文字列)

      ・Actionとして、以下のメッセージを設定
       文字列: 解錠しました

      この状態で、Wena3からこの作成したRiiiverのコマンドを実行したところ、Wena3側には「解錠しました」と表示されますが、実際解錠はされておらず、GASの実行ログにも実行された形跡がありません

      考えられる不備等ありますでしょうか?よろしくお願いします。

      削除
    5. Riiiverは使っていないので見当がつきません。キノさんがご存知かもしれませんね。そこまで行っていればこのサンプルとは無関係なので、RiiiverのGAS GETメッセージの書き方自体を調べると良いと思います。

      削除
    6. ありがとうございます。そうですね、もしキノさんがご存じでしたら幸いです。RiiiverのGAS GETメッセージも調べてみます。

      削除
    7. 色々調べていて解決できましたので報告します。GASでのアプリケーションデプロイ時の実行ユーザー設定の問題でした。下記にしたところ動きました。ありがとうございました。
      ・次のユーザーとして実行:自分
      ・アクセスできるユーザー:全員

      削除
  10. 有益な情報の共有ありがとうございます
    コードコピペ後、GAS上で実行すると以下のエラーが出ます
    TypeError: Cannot read property 'hasOwnProperty' of undefined

    知識がほとんどない状態で作業しているので、調べても解決策がわからず困っています。
    もしお分かりであれば教えていただきたいです
    書いてあるとおり間違いなく作業はしたはずです、、、

    返信削除
    返信
    1. prepare()実行時ですか?test()ですか?
      TypeErrorの出た行数はどこですか?(エラーの下に出ると思います)

      削除
    2. TypeError: Cannot read property 'hasOwnProperty' of undefined
      (匿名) @ cmac.min.js.gs:6
      (匿名) @ cmac.min.js.gs:6
      このようにエラーがでています
      クリックすると、cmac.min.js.gsの6行目の最初のfunction(t){ }と最初の(CryptoJS)にカーソルが出ます

      削除
    3. どの関数を実行していますか?
      showPropsでプロパティが正常に保存されていることは確認できますか?

      削除
  11. prepare()でもtest()でも、showProps()でも同じようになります!

    返信削除
    返信
    1. 事前準備2の画像の通りcryptojs-aes.minがcmac.minの上に来ていますか?

      削除
    2. 順番を変更したところ解決いたしました。
      アルファベット順に並べ替えるボタンが存在していたので、順番は関係ないものなのかなと思ってしまいました
      まったく無知なところでの作業でしたので、助かりましたありがとうございます!

      削除
  12. URL内の二重引用符(")をすべて外してみてください。その方式でアクセスする場合、値を二重引用符で囲む必要はありません。

    返信削除
  13. 返信有難うございます
    無事に動作確認出来ました

    次のステップとしてIFTTTを利用して
    Webhook を利用して施錠・開錠を設定しています

    URL
    https://script.google.com/macros/s/***/exec

    Method
    POST

    Content Type
    application/json

    Additional Headers
    空欄のまま

    Body
    {"myKey":"****","deviceName":"セサミ","command":0,"user":"ウェブ開錠" }

    トリガーの実行はされるのですが
    Your server returned a 401
    のエラーが返って来ます

    どこの設定に誤りがあるか解るようでしたら
    ご指導の程宜しくお願い致します。

    返信削除
  14. ウェブアプリのユーザー認証に誤りがあるようです。
    【3. ウェブアプリ化】の項を再度確認してください。
    本文に書いてあるのですが、わかりづらいのか以前にも同じ箇所に起因するエラー報告があったので一応画像も追加しておきました。

    返信削除

【SESAMEサイクル(SESAMEシリーズ)】 NFCタグをエミュレートするアプリをつくった (Android)

セサミのNFCタグ機能を実際のタグなしで自由に発動させたくて、アプリを作った話。 ( ページ下部より APKダウンロードできます )  【まえがき】 うちでは玄関のセサミ3は前回記事で書いたGASで運用していてNFCタグ機能は使っていないのだけど、最近セサミサイクル(ママチャリ)...