2021/10/30 更新
複数台操作の不具合の指摘をうけ、修正がてらプロパティ周りの仕様を大幅変更。
【まえがき:APIが刷新されていた】
SESAME用のAPI* がひっそりと上がっていた(正式発表はまだ?)ので、Google Apps Script からいじれるようにしてみた。
*追記:セサミ3/4/5などに対応。
従来のものとは仕様が変更され、導入のハードルがすこし上がった感じ。
今回は、以前書いたセサミミニ用のスクリプトから乗り換えやすいよう、使用方法を極力継承してセサミ3以降に対応した新しいスクリプトを書いてみた。
【事前準備1:必要な各種データを入手】
1. 公式のユーザーページ(https://biz.candyhouse.co)にログインし、「開発者向け」ページでAPIキーを取得。
2. 以下のいずれかを行う。(追記:現在はB推奨)
A. セサミアプリでマネジャー権限のQRコードを発行し、好みのQRコードリーダーで読み取り、中身のテキスト(ssm://UI? から始まる長い文字列)をコピーしておく。
(以前はSecret Keyなどを簡単に取得できず、このようにするしかなかった)
B. ユーザーページの 「個人で登録済みのデバイス」ボタンから任意のデバイスを選択し、UUIDとSecret Keyを取得。
【事前準備2:外部JavaScriptを追加】
施解錠操作用の署名生成がややこしくて自力では困難なため、artjombさんの cryptojs-extension を拝借する。
1. まずはスクリプトファイルを新規作成
(Googleドライブ 新規→その他→Google Apps Script)
- lib/cryptojs-aes.min.js
- build/cmac.min.js
の内容をそれぞれ丸ごとスクリプトファイルに追加(コピペ)する。
ファイル + からスクリプトを追加できる。
※初期状態で表示されるテキストは不要なので、テキストエリアを空白にしてから貼り付け
名前はなんでも良いけれどわかりやすくそのまま。
【重要】3件のファイルは必ず上記画像の順で配置する。
【汎用サンプルコード】
ようやくメインのコード。
はじめに、「プロジェクトの設定」で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:88const 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情報デコード Corelet hx = (data, start, end) => {return Utilities.base64Decode(data).slice(start, end + 1).map(function(chr){return (chr+256).toString(16).slice(-2)}).join('');}
【使用方法】
【1. プロパティを登録】
まず初回のみプロパティを登録する。
【2. 動作チェック】
- function test() 内の情報を適宜書き換える。
- 関数メニューで「test」を選択し、実行ボタンを押す。
成功したら200が返り、セサミが動く。
うまく行かない場合は…
主な返り値と原因の可能性
404:サーバーダウン OR ルーターやWi-Fiモジュール等の問題かも。
403:APIキーが間違っているかも。
502:UUIDが間違っているかも。
200なのに動作しない:SecretKeyが間違っているかも。
【3. ウェブアプリ化】
途中で「承認が必要」と出た場合は流れに従って承認する。
(ログイン→詳細→安全ではないページに移動→許可)
最後に表示されたウェブアプリURLをコピーしておく。
【4. 運用】
あとは従来同様、このURLに適切にリクエストを送れば良い。
より汎用的にするためPOST、GET両方用意しているけれど、基本的にはPOSTでOK。
パラメータは従来からひとつ増え、myKey、deviceName、command、userの4つ。
- myKey:prepareで準備した任意のキー。一致しないとはじかれる。
- deviceName:セサミの名前。複数台同時に動かしたい場合は、カンマ区切りで「玄関,玄関2,倉庫」のように。
- command:数字で、0~2のいずれか
0=解錠 1=施錠 2=トグル - user:通知や履歴に表示される名前。省略可。省略した場合「ウェブアプリ」となる。
※userに特定の文字列を指定すると一部が文字化けする事象を確認済み。特定の文字というわけでなく、規則性をつかめず。現時点で詳細不明。
【最後に:スクリプトをあとから編集した場合】
スクリプトの編集をウェブアプリに反映させるには再デプロイが必要。
1. 編集を保存
2. 「デプロイ」→「デプロイを管理」
3. 編集ボタン(右上のペンアイコン)を押して、「バージョン」から新バージョンを選択
4. デプロイ実行
以上。
(注意・免責)リスクを管理し、あくまで自己責任で使用してください。
【余談】
今回は施解錠のみのスクリプトのため以上となるけれど、新APIはGETも刷新されていて、履歴やバッテリ電圧、サムターン角度なども取得できるようになっていた。アイデア次第で便利に使えそうだ。
"ReferenceError: CryptoJS is not defined"になります。
返信削除別の方からは問題なく使用できた報告を受けているので、おそらく外部JavaScriptの追加方法が間違っています。
削除コメント失礼します。
返信削除非常に有益な情報をありがとうございます。
参考にさせていただいております。
初学者のためぜひアドバイスいただきたく存じます。
GASの設定までは問題なく行えたのですが、POST送信がうまく機能せず困惑しております。
下記内容をwebhookでPOSTしているのですが、不備があればご教授いただきたく存じます。
POST 生成されたウェブアプリURL
Content Type application/json
Body
[{
"myKey": "任意の数字",
"deviceName": "セサミ", //アプリでセサミに名称変更済のため
"command": "0",
"user":"ウェブアプリ"}
]
ご使用の環境がわからないため断定はできませんが、通常Body全体を角括弧で囲むことはしないと思いますので、一度 [] を外してみてください。
削除例として、IFTTTのアクションでWebhookを使用する場合、Bodyの入力欄は以下のようにします。
{"myKey":"*****","deviceName":"セサミ","command":"0","user":"ウェブアプリ"}
お返事ありがとうございます。
削除無事に作動確認できました、ご教授ありがとうございました。
閉開の履歴を取得してwebhookを使い玄関の電気のオンオフを考えています。
返信削除セサミオープンで電気がついて
鍵を締めたら自動で電気オフができたら便利かなと思いました。
公式に確認したら直接のwebhookは開発中で年内にはとの回答でした。
お手好きにでもgasで履歴を取得してwebhookなどの記事を書いて頂けると助かります。
現状、GASで公式webhookの動作を代替することは不可能と思われます。
削除履歴取得は可能なのですが、高頻度で叩くとすぐ429 Too Many Requestsエラーになるため、履歴をもとに即時に何かさせるのは現実的ではありません。
また公式webhookですが、旧版では手動開閉時にも発動する仕様でしたので、新版も同様であれば、「施錠で消灯」というルールでは帰宅時手動施錠した際、玄関にいる状態で真っ暗になってしまうのではと想像します。
もし、外出時の施錠と帰宅時の解錠のみをAPI(GAS経由)で行う予定であれば、GASで同時に照明も操作してしまうのが確実だと思います。
もしくはIoTではないですが、人感センサー付き電球も環境次第でアリだと思いますよ。うちは廊下や玄関はそうしています。
そうでしたかすいませんでした。
削除閉めた際に電気がオフになるのはオートロックにしているので玄関にいない時になるので大丈夫です。
Gasってなんかしらのアプリから開け締めできますでしょうか
人感センサーは実家で使ってますが玄関直の狭いワンルームなのでちょくちょくついてしまいます。
いえいえ。
削除なるほど、そういうことですね。
GASの実行はHTTPリクエストを送信できるアプリから行えます。iPhoneなら標準の「ショートカット」アプリ、Androidなら「Tasker」など多数ありますので詳しくは調べてみてください。
ただ、オートロックはGASではリアルタイム検知できないので、別案が必要です。パッと思いつくのは、帰宅時一発実行で「解錠→点灯→待機→消灯」をするプログラムでしょうか。これならGASで可能だと思います。
そうですよね
削除年内のwebhookを期待して待ちます。
ありがとうございます
質問失礼します
返信削除Sesame4で2台同時にIFTTTで解錠しようとしています
1台であれば動作しますが、2台にするとSKを入れた方のSesameのみ動作します
それぞれSKが異なるようですが、2台同時に動作させる方法をご教授願います
bodyは、{"myKey":"*****","deviceName":"セサミ,セサミ2","command":"0","user":"ウェブアプリ"}としています
質問失礼します
返信削除Sesame4で2台同時に動作させようとしています
色々とトライしましたが1台のみであれば動作しますが、2台にするとSKを入れたSesameのみ動作します
SKは一つ一つ異なると思いますが、同時動作させる方法がありましたらご教授願います
Bodyは{"myKey":"*****","deviceName":"セサミ,セサミ2","command":"0","user":"ウェブアプリ"}としています
お手数ですがご確認お願いします
貴重な情報ありがとうございます。これは設計ミスですね。
削除一台しか所有していないのに公開にあたり汎用仕様を求めたせいで、複数台の動作検証ができていませんでした。
skをデバイスごとにプロパティに保存して呼び出すよう書き換えれば動作すると思います。近いうちに修正更新します。
ご指摘ありがとうございました。
返答ありがとうございます
削除修正お待ちしています
修正しました。お手数ですがコードのコピペから再度試してみてください。
削除当該箇所を修正し、データ準備の方法を全体的に見直しています。
それに伴い、事前準備1の項も加筆修正しました。確認をお願いします。
もしまだ問題があればご報告ください。
早々のご対応ありがとうございました
削除問題なく2台でも動作しております
ブログを拝見し、見よう見まねで挑戦したところ、【1. プロパティを登録】のところでエラーが発生し進めません。
返信削除「****」の箇所はすべて入力しているつもりですが、事前準備A組、B組に不備があるのでしょうか?
お時間がありましたら、ご支援のほどよろしくお願いいたします。
----以下エラー内容----
エラー
Exception: Could not decode string.
hx @ function.gs:114
(匿名) @ function.gs:101
analyzeQR @ function.gs:98
prepare @ function.gs:12
自前のQRでは成功してしまうので、原因究明にご協力ください。
削除一部の値に対してのバグかもしれません。
まず、テキストが ssm://UI?~~~&sk=*****&~~~ の構造になっているか確認してください。&sk=から次の&までの*で示した箇所の値が重要です。
この箇所に英数字以外の記号などが含まれていれば、その文字を教えてください。ヒントになるかもしれません。よろしくおねがいします。
ご返事、ありがとうございます。
返信削除バグかもとご心配をおかけしましたが、見直したところssm://UI? から始まる長い文字列に誤りがあり、修正したところエラーは無くなりました。
大変申し訳ございませんでした。
よかったです。
削除正に求めていた情報で非常に感激いたしました。ありがとうございます。
返信削除SESAME3をウェブ上で操作出来るまでには至りましたが、目的であるwena3からRiiiverを使ってGASを実行することがうまくいきません。
デプロイID:生成されたものをコピー
パラメータ:{"myKey":"*****","deviceName":"セサミ","command":"0","user":"ウェブアプリ"}
上記のようにしているのですが、考えられる不備等ありますでしょうか?
宜しければお手漉きの際にでもお教えいただけないでしょうか?
デプロイIDではなくウェブアプリURLをコピーして使用します。
削除パラメータの方は問題ないかと思います。
早速のご返信ありがとうございます。
削除入力箇所には
URL:https://script.google.com/macros/s/XXX/exec?YYY
【デプロイID(XXX)】
【パラメータ(YYY)】
と記載があります。
また、デプロイID入力箇所にウェブアプリURLを入力してみても動作しませんでした。
そうなんですね。通常HTMLリクエストではデプロイIDの入力は不要なので、その仕様はちょっとわかりかねます。もしかすると実行可能APIとしてデプロイする方法かもしれませんが、詳しくありません。(Google Cloud Platformへの決済情報登録が必要だった気がします)
削除一応関連すると思われる記事を見つけました。これ以上のことはわかりません。
https://jellyware.jp/riiiver/contents/contents20.html
Riiverについて少し調べてみましたが、IFTTT Webhookも使えるようなので、もし経路にこだわりがなければIFTTT経由を試してみてください。
「Webhookトリガー → Webhookアクション」でHTMLリクエストが使用できると思います。
そうですか…。
返信削除迅速なご返信ありがとうございました。
あきらめることにします。
自身で色々試したところ、解決致しました。
削除パラメータに問題がありました。
myKey=*****&deviceName=セサミ&command=0&user=ウェブアプリ
としたことで動作しました。
お騒がせしました。
なるほどGETだったんですね。良かったです。
削除その書き方はPOSTではなくGETメソッドです。前提がPOSTの話と思っていてパラメータは問題ないとお答えしてしまいました。
無事動作して良かったです。
doPOSTとdoGETの違いの理解が浅いのですが、GETに対応していただいていたおかげかと思います。本当にありがとうございます。
削除あとはriiiverのGASの項目が【GAS実行】と【GAS GETメッセージ】がありまして、【GAS実行】ではどのようにしても無理でした。
【GAS GETメッセージ】で上記設定で動作しましたので、今後同じ目的の方がおられましたら参考にしていただけたらと思います。
本当にありがとうございました。
貴重な情報ありがとうございます。
削除私も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の実行ログにも実行された形跡がありません
考えられる不備等ありますでしょうか?よろしくお願いします。
Riiiverは使っていないので見当がつきません。キノさんがご存知かもしれませんね。そこまで行っていればこのサンプルとは無関係なので、RiiiverのGAS GETメッセージの書き方自体を調べると良いと思います。
削除ありがとうございます。そうですね、もしキノさんがご存じでしたら幸いです。RiiiverのGAS GETメッセージも調べてみます。
削除色々調べていて解決できましたので報告します。GASでのアプリケーションデプロイ時の実行ユーザー設定の問題でした。下記にしたところ動きました。ありがとうございました。
削除・次のユーザーとして実行:自分
・アクセスできるユーザー:全員
有益な情報の共有ありがとうございます
返信削除コードコピペ後、GAS上で実行すると以下のエラーが出ます
TypeError: Cannot read property 'hasOwnProperty' of undefined
知識がほとんどない状態で作業しているので、調べても解決策がわからず困っています。
もしお分かりであれば教えていただきたいです
書いてあるとおり間違いなく作業はしたはずです、、、
prepare()実行時ですか?test()ですか?
削除TypeErrorの出た行数はどこですか?(エラーの下に出ると思います)
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)にカーソルが出ます
どの関数を実行していますか?
削除showPropsでプロパティが正常に保存されていることは確認できますか?
prepare()でもtest()でも、showProps()でも同じようになります!
返信削除事前準備2の画像の通りcryptojs-aes.minがcmac.minの上に来ていますか?
削除順番を変更したところ解決いたしました。
削除アルファベット順に並べ替えるボタンが存在していたので、順番は関係ないものなのかなと思ってしまいました
まったく無知なところでの作業でしたので、助かりましたありがとうございます!
URL内の二重引用符(")をすべて外してみてください。その方式でアクセスする場合、値を二重引用符で囲む必要はありません。
返信削除返信有難うございます
返信削除無事に動作確認出来ました
次のステップとして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
のエラーが返って来ます
どこの設定に誤りがあるか解るようでしたら
ご指導の程宜しくお願い致します。
ウェブアプリのユーザー認証に誤りがあるようです。
返信削除【3. ウェブアプリ化】の項を再度確認してください。
本文に書いてあるのですが、わかりづらいのか以前にも同じ箇所に起因するエラー報告があったので一応画像も追加しておきました。