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も刷新されていて、履歴やバッテリ電圧、サムターン角度なども取得できるようになっていた。アイデア次第で便利に使えそうだ。


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

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