はじめに
セサミ4は、Google Apps Scriptから制御することもできます。
セサミ4の通知機能とリモートからの制御機能は十分なのであまりGASを使った制御には必要性を感じないところです。
でも、普段オートロックなしで使っているので、たまに夜寝る前に鍵をかけ忘れてしまうことがあります。
セサミ4の機能には、時間が来たら鍵を閉める、時間が来たら鍵を開けるといった、時間をトリガーとした制御はありません。
そこで今回は、時間が来たら鍵を閉める処理をGASを使って実装しようと思います。
という事で、今日の課題はこちらです。
今回の課題
- 夜になったら自動的に鍵を閉めるようにしたい。
セサミ4をGASで制御
必要な情報の準備
セサミID(sesami_id)
制御するセサミを特定するためにIDを取得します。
セサミ4のアプリを開き、設定画面中のUUIDをsesami_idとして使用します。
セサミ4 制御API (x-api-key)
下記サイトにログインして、API KEYをコピーします。
次の章で示すコードの「sesami_id」として使用します。
ちなみに、API documentは以下にあります。
秘密鍵(key_secret_hex)
セサミ4のアプリを開き、設定画面から「このセサミの鍵をシェアする」を選択し、「ゲスト」のQRコードを表示させます。
QRコードのデコードはアプリでも以下のサイトで実行してもOKです。
QRコードをデコードすると、以下のように表示されます。
ssm://UI?t=sk&sk=xxxxxxxxxxxxxxxxxxxxxxxxxx&l=2&n=xxxxxxxxxxxxxxxxxxxxx
このままだとURLエンコードされており、Base64で使われる文字である「+」が「%2F」、「/」が「%2B」と表現されているため、Base64でデコードできません。
QRコードの読み取り結果をコピーして、以下のサイトでURLデコードさせます。
次に、「sk=」の次から「&l=」の前までをコピーして、次の章で示すコードの「SK」の値に設定します。
ちなみに、Base64をデコードしたByteデータの2byte目から16byteを16進表示にしたデータが秘密鍵として使われます。
Base64でデコードして16進表示の確認をしたい場合は以下のサイトでデコードできます。
メモ
実は、ここまで事前準備としてセサミIDと秘密鍵の取得方法を説明したのですが、QRコードをデコードした中に、セサミIDと秘密鍵がはいっていることがわかりました。
なので、ここの全てのコードではQRコードをデコード(URLデコード後)したデータを設定値に与えて、セサミIDと秘密鍵を抽出するようにしています。
セサミの状態確認
API ドキュメントにセサミの状態を取得するためのスキーマは以下のように定義されています。
GET: "https://app.candyhouse.co/api/sesame2/{sesame2_uuid}"
Parameters:
name = "x-api-key", location = "header",des="your api-key"
name = "sesame2_uuid", location = "path",des="sesame UUID"
URLにセサミID(sesame2_uuid)を指定し、headerにx-api-keyでAPIキーを指定し、GETで呼び出せばセサミの状態が取得できます。
コードは以下のようになります。
const SESAME_ID = "<ここにセサミUUIDを指定してください>"; // セサミUUID
let getState = async () => {
// option作成
let opt = {
method: 'get',
headers: {'x-api-key': `<ここにAPI KEYを指定してください>`},
contentType: "application/json"
};
let res = UrlFetchApp.fetch(`https://app.candyhouse.co/api/sesame2/${SESAME_ID}`, opt);
Logger.log('code:' + res.getResponseCode());
Logger.log('content:' + res.getContentText());
}
res.getContentText()を確認すると、以下のようにセサミの状態がJSON形式で得られます。
{
"batteryPercentage":100,
"batteryVoltage":6.116129032258065,
"position":766,
"CHSesame2Status":"unlocked",
"timestamp":1643917480,
"wm2State":true
}
解錠・施錠
API ドキュメントにセサミの鍵を操作するためのスキーマは以下のように定義されています。
POST: "https://app.candyhouse.co/api/sesame2/${sesame_id}/cmd"
BODY: {
cmd: cmd,
history: base64_history,
sign: sign
}
Parameters:
name = "x-api-key", location = "header",des="your api-key"
name = "sesame2_uuid", location = "path"
name = "cmd", location = "body",des="開閉コマンドコード"
name = "history", location = "body",des="履歴"
name = "sign", location = "body",des="署名"
cmd:
toggle:88 / lock:82 / unlock:83
URLにセサミID(sesame2_uuid)を指定し、headerにx-api-keyでAPIキーを指定し、コマンド(鍵の開錠・施錠)、履歴に表示する文字、シグネチャをbodyとしてPOSTすれば鍵を解錠・施錠できます。
bodyの中身は以下の通りです。
cmd : 施錠:82、解錠:83、トグル:88
history:履歴に表示するための文字(半角21文字まで)
sign:シグネチャ(これを作るのがちょっとめんどい)
解錠するためのコードは以下のようになります。(cmdに82の解錠を指定しています)
セサミUUID、SK、API KEYについては、こちらで取得方法を確認してください。
const SESAME_ID = "<ここにセサミUUIDを指定してください>"; // セサミUUID
const SK = '<ここにQRコードの「sk=」の次から「&l=」の前までを設定してください>';
let lockKey = async () => {
let cmd = 82; //(toggle:88,lock:82,unlock:83)
let base64_history = Utilities.base64Encode('Web API'); // 履歴に残す内容
// 秘密鍵取得
decoded = Utilities.base64Decode(SK, Utilities.Charset.UTF_8);
key_secret_hex = bytesToHex(decoded.slice(1,17));
// body作成
let sign = generateRandomTag(key_secret_hex);
let body = {
cmd: cmd,
history: base64_history,
sign: sign
};
// option作成
let opt = {
method: 'post',
headers: {'x-api-key': `<ここにAPI KEYを指定してください>`},
contentType: "application/json",
payload: JSON.stringify(body)
};
// 鍵の制御
let res = UrlFetchApp.fetch(`https://app.candyhouse.co/api/sesame2/${SESAME_ID}/cmd`, opt);
Logger.log('code:' + res.getResponseCode());
Logger.log('content:' + res.getContentText());
}
function generateRandomTag(secret) {
// * key:key-secret_hex to data
let key = CryptoJS.enc.Hex.parse(secret);
// message
// 1. timestamp (SECONDS SINCE JAN 01 1970. (UTC)) // 1621854456905
// 2. timestamp to uint32 (little endian) //f888ab60
// 3. remove most-significant byte //0x88ab60
const date = Math.floor(Date.now() / 1000);
const dateDate = new DataView(new ArrayBuffer(4));
dateDate.setUint32(0, date, true);
const message = CryptoJS.enc.Hex.parse(dateDate.getUint32(0).toString(16).slice(2,8));
return CryptoJS.CMAC(key, message).toString();
}
function bytesToHex(bytes) {
for (var hex = [], i = 0; i < bytes.length; i++) {
var current = bytes[i] < 0 ? bytes[i] + 256 : bytes[i];
hex.push((current >>> 4).toString(16));
hex.push((current & 0xF).toString(16));
}
return hex.join("");
}
QRコードをデコードして取得したデータをUtilities.base64Decode()を用いてBase64でデコードしてByte配列を取得し、2byte目から17byte目までの16byteを16進に変換しています。16進に変換するためにbyteToHex()関数を定義して使用しています。
signで指定するシグネチャを作るのが少し面倒なので、CryptoJSを使用します。
以下より「lib/cryptojs-aes.min.js」と「build/cmac.min.js」の二つをプロジェクトに追加します。
プロジェクトにスクリプトを追加し、ファイル名を変更してcryptojs-aes.min.jsとcmac.min.jsの内容をコピペします。
ここまで出来たら、lockKey()を実行することで鍵が閉まることを確認できると思います。
23時に鍵施錠
毎日23時に鍵を施錠するために、トリガー設定を行います。
Google Apps Scriptのトリガー設定では、毎日1回実行させるタイマーを設定できるのですが、GUIを使用して「何時何分に起動する」という設定はできません。
一方、プログラムからは「何時何分に起動する」という指定ができるので、毎日23時に起動するトリガー設定のコードを書きます。
トリガー起動時間後も登録したトリガー情報は残るので、次の日のトリガーを設定する前に、当日のトリガーを削除します。
/*
* 毎日定時実行トリガー設定(サンプル)
*
* 1日1回定期的に実行させるためのサンプル
* デフォルトではTimeZoneがAmerica/New_YorkのGMT-5なので、
* そのままではトリガを設定した時刻が意図した時間に動作しない。
* そのため、プロジェクトの設定からappsscript.jsonを表示させて、
* TimeZoneをJSTに変更しなければならない。
*
* 2022/01/08 Created by N.Sekiya
*/
var RUNDAILY_HOUR = 23 // 毎日実行するトリガーの時間設定
var RUNDAILY_MIN = 00 // 毎日実行するトリガーの分設定
function runDaily() {
Logger.log('毎日定時トリガー起動');
//-------------------------------------------
// ここで施錠する関数を呼び出す
//-------------------------------------------
//-------------------------------------------
// 起動したトリガー設定の削除と次回のトリガーを設定する
//-------------------------------------------
deleteTrigger(); // 設定されているトリガーを全て削除する
setTrigger(); // 新しい時間でトリガーを再設定する
}
// トリガーを登録する
function setTrigger() {
// 起動するトリガーの時間設定
const triggerDay = new Date();
Logger.log('本日:' + triggerDay);
triggerDay.setDate(triggerDay.getDate() + 1); // 一日進める
triggerDay.setHours(RUNDAILY_HOUR); // 毎日実行するトリガーの時間設定
triggerDay.setMinutes(RUNDAILY_MIN); // 毎日実行するトリガーの分設定
Logger.log('設定トリガ:' + triggerDay);
// runDaily()を起動するトリガー登録
ScriptApp.newTrigger("runDaily")
.timeBased()
.at(triggerDay)
.create();
}
//プロジェクト内のすべてのトリガーを削除
function deleteTrigger() {
//プロジェクトのすべてのトリガーを取得して削除する
const trgs = ScriptApp.getProjectTriggers();
for(const trg of trgs){
ScriptApp.deleteTrigger(trg); // 設定済みトリガー削除
}
}
最初に1回上記コードのrunDaily()を起動すれば、毎日23時にトリガーにより起動するようになります。
処理の流れは以下の通りです。
- 23時にトリガーによりrunDaily()が呼び出される
- 鍵を施錠する
- プロジェクトに設定されているトリガーをすべて削除する
- 新たに次の日の23時に起動するためのトリガーを登録する
毎日1回トリガーにより起動する方法についての詳細は、以下の記事を参照ください。
全てのコード
QRには、QRコードをデコードして得られた「ssm://」から始まる全ての文字列を設定してください。この中に含まれる秘密鍵とセサミを識別するUUIDをanalyzeQR()関数で取得するようにしました。
const PRJ_NAME = 'CtrlSesame4'; // logOut用
const QR = 'ssm://UI?t=sk&sk=xxxxxxxxxxxxxxxxxxxxxxxx&l=x&n=xxxxxx'; // <QRコードのデコード結果に置き換えてください>
const API_KEY = '<ここにAPIキーを指定してください>';
var uuid;
var sec_key;
// 毎日23時に鍵を閉める処理
function lockOnTime(){
lockKey(); // 鍵を閉める
deleteTrigger(); // トリガーを全て消す
setTrigger("lockKey", 23, 00); // トリガー再設定(翌日の23:00にlockKey()が動くように設定する)
}
function doGet(e) {
lockKey();
return "";
}
let lockKey = async () => {
let cmd = 82; //(toggle:88,lock:82,unlock:83)
let base64_history = Utilities.base64Encode('WebAPI'); // 履歴に残す内容
// UUID及び秘密鍵取得
analyzeQRCode(QR);
logOut(PRJ_NAME, 'uuid=' + uuid);
logOut(PRJ_NAME, 'sec_key=' + sec_key);
// body作成
let sign = generateCmacSign(sec_key);
let body = {
cmd: cmd,
history: base64_history,
sign: sign
};
// option作成
let opt = {
method: 'POST',
headers: {'x-api-key': API_KEY},
muteHttpExceptions: true,
payload: JSON.stringify(body)
};
let res = UrlFetchApp.fetch(`https://app.candyhouse.co/api/sesame2/${uuid}/cmd`, opt);
}
let getState = async () => {
analyzeQRCode(QR);
// option作成
let opt = {
method: 'GET',
headers: {'x-api-key': API_KEY},
muteHttpExceptions: true,
contentType: "application/json"
};
let res = UrlFetchApp.fetch(`https://app.candyhouse.co/api/sesame2/${uuid}`, opt);
Logger.log(PRJ_NAME, 'code:' + res.getResponseCode() + ' content:' + res.getContentText());
}
function generateCmacSign(secret) {
// message
// 1. timestamp (SECONDS SINCE JAN 01 1970. (UTC)) // 1621854456905
// 2. timestamp to uint32 (little endian) //f888ab60
// 3. remove most-significant byte //0x88ab60
const date = Math.floor(Date.now() / 1000);
const dateDate = new DataView(new ArrayBuffer(4));
dateDate.setUint32(0, date, true);
const msg = CryptoJS.enc.Hex.parse(dateDate.getUint32(0).toString(16).slice(2, 8));
// * key:key-secret_hex to data
const key = CryptoJS.enc.Hex.parse(secret);
return CryptoJS.CMAC(key, msg).toString();
}
// QRコードから秘密鍵とセサミUUIDを取得する
function analyzeQRCode(qr){
const prm = qr.slice(qr.indexOf('?')+1).split('&');
prm.forEach(function(p){
if(p.indexOf('sk=') == 0){
const sk = p.slice(3);
const dec = Utilities.base64Decode(sk, Utilities.Charset.UTF_8);
uuid = bytesToHex(dec.slice(83, 87)) + '-'
+ bytesToHex(dec.slice(87, 89)) + '-' + bytesToHex(dec.slice(89, 91)) + '-'
+ bytesToHex(dec.slice(91, 93)) + '-' + bytesToHex(dec.slice(93, 99));
sec_key = bytesToHex(dec.slice(1,17));
}
});
}
function bytesToHex(bytes) {
for (var hex = [], i = 0; i < bytes.length; i++) {
var current = bytes[i] < 0 ? bytes[i] + 256 : bytes[i];
hex.push((current >>> 4).toString(16));
hex.push((current & 0xF).toString(16));
}
return hex.join("");
}
// トリガー再登録
const setTrigger = (func, h, m) => {
// homeLockCloseを翌日の23:00に起動するトリガーを設定
const triggerDay = new Date();
triggerDay.setDate(triggerDay.getDate() + 1); // 一日進める
triggerDay.setHours(h); //23時
triggerDay.setMinutes(m); //00分
ScriptApp.newTrigger(func)
.timeBased()
.at(triggerDay)
.create();
}
//全トリガー削除
const deleteTrigger = () => {
//プロジェクトのすべてのトリガーを取得して削除する
const triggers = ScriptApp.getProjectTriggers();
for(const trigger of triggers){
ScriptApp.deleteTrigger(trigger); //トリガーの削除
}
}
動作確認
一度、lockOnTime()関数を実行すると、翌日の23:00に鍵を閉める関数lockKey()が登録されます。
その後は、毎日23:00に鍵を自動的に閉めますので、ほったらかしでOKです。
まとめ
いかがでしたでしょうか。
23時になると自動的に鍵が閉まるようになります。時間はお好みによって変えてください。
これで、鍵を閉めたことを心配することなく、安心して寝られますね。
お試しください。
では、今日も良い一日を。