作った背景
私自身が忘れっぽい性格なので、たまに自宅のカギを掛けずに出かけてしまうことがあります。
これを解決するため、カギの状態を監視して開いたままのときはLINE通知するアプリケーションを作ってみました。
家のカギを開け放してると母ちゃんに怒られるクソボットを作りました😇 pic.twitter.com/xoIkgLObfw
— かおなが (@kaonaga9) April 3, 2022
成果物イメージ
一定時間ごとに自宅のカギの状態を監視して、開いていれば施錠を促すLINEを送ります。
Node.jsのアプリケーションをHerokuにデプロイします。
用意したもの
- SESAME 4
-
Wi-Fiモジュール
インターネット上から自宅のセサミにアクセスするために必要です。
SESAMEをAPIから操作する方法
SESAMEではWeb APIが無料で提供されていて、自作プログラムから施錠解錠することができます。
詳しい使い方やセットアップの方法は以下の記事にまとめてますのでご参照ください。
Node(Express)でWebサーバーを構築
index.js
const express = require('express');
const app = express();
const PORT = process.env.PORT || 5000;
const sesame = require('./sesame.js');
const line = require('./line.js')
app.use(express.json())
app.use(express.urlencoded({
extended: true
}))
app.get('/', (req, res) => {
res.json({
message: "Application running..."
});
});
// 家のSESAMEの開閉状態を取得する
app.get('/status', async (req, res) => {
const { data } = await sesame.get_status();
res.json(data);
});
// カギが開いてればLINE通知する
app.get('/remindme', async (req, res) => {
const { data } = await sesame.get_status();
if (data.CHSesame2Status == 'unlocked') {
const result = await line.notify();
return res.json({
message: "Notification sended!"
})
};
res.json({
message: "The key is locked"
})
});
// Webhook
app.post('/webhook', async (req, res) => {
// Signature検証
if (!line.validateSignature(req.body, req.headers['x-line-signature'])) {
return res.status(401).json({
message: "Invalid signature received"
})
}
// postbackイベントを処理する
if (req.body.events.length > 0 && req.body.events[0].type == "postback") {
const result = await sesame.lock_cmd();
}
res.sendStatus(200);
})
app.listen(PORT, () => {
console.log(`Listening on ${PORT}!`);
});
sesame.js
const axios = require('axios');
const aesCmac = require('node-aes-cmac').aesCmac;
const sesame_id = process.env.SESAME_UUID;
const sesame_api_key = process.env.SESAME_API_KEY;
const key_secret_hex = process.env.KEY_SECRET_HEX;
exports.get_status = async () => {
const result = await axios({
method: 'get',
url: `https://app.candyhouse.co/api/sesame2/${sesame_id}`,
headers: { 'x-api-key': sesame_api_key }
})
return result;
};
exports.lock_cmd = async () => {
const cmd = 82;
const history = "Locked via LINE bot";
const base64_history = Buffer.from(history).toString('base64');
const sign = generateRandomTag(key_secret_hex)
const result = await axios({
method: 'post',
url: `https://app.candyhouse.co/api/sesame2/${sesame_id}/cmd`,
headers: { 'x-api-key': sesame_api_key },
data: {
cmd: cmd,
history: base64_history,
sign: sign
}
})
return result;
};
exports.unlock_cmd = async () => {
const cmd = 83;
const history = "Unlocked via LINE bot";
const base64_history = Buffer.from(history).toString('base64');
const sign = generateRandomTag(key_secret_hex);
const result = await axios({
method: 'post',
url: `https://app.candyhouse.co/api/sesame2/${sesame_id}/cmd`,
headers: { 'x-api-key': sesame_api_key },
data: {
cmd: cmd,
history: base64_history,
sign: sign
}
})
return result;
};
function generateRandomTag(secret) {
const key = Buffer.from(secret, 'hex');
const date = Math.floor(Date.now() / 1000);
const dateDate = Buffer.allocUnsafe(4);
dateDate.writeUInt32LE(date);
const message = Buffer.from(dateDate.slice(1, 4));
return aesCmac(key, message);
}
line.js
const line = require('@line/bot-sdk');
const config = {
channelAccessToken: process.env.CHANNEL_ACCESS_TOKEN,
channelSecret: process.env.CHANNEL_SECRET,
};
const client = new line.Client(config);
const userId = process.env.LINE_USER_ID;
// Webhookの署名検証
exports.validateSignature = (body, signature) => {
return line.validateSignature(Buffer.from(JSON.stringify(body)), config.channelSecret, signature);
}
// プッシュ通知を送る
exports.notify = async () => {
const message = {
type: "flex",
altText: "カギが開いています",
contents: flexContents
};
await client.pushMessage(userId, message)
.catch((err) => {
console.log(err);
});
}
// フレックスメッセージ
const flexContents = {
type: "bubble",
body: {
type: "box",
layout: "vertical",
contents: [
{
type: "text",
text: "カギあけっぱなし!",
weight: "bold",
size: "md"
}
]
},
footer: {
type: "box",
layout: "vertical",
spacing: "sm",
contents: [
{
type: "button",
style: "primary",
height: "sm",
action: {
type: "postback",
label: "LOCK",
data: "lock"
}
},
{
type: "box",
layout: "vertical",
contents: [],
margin: "sm"
}
],
flex: 0
}
}
Herokuにデプロイする
アプリケーションをHerokuにデプロイします。
GitHubのREADMEに "Deploy to Heroku" ボタンを設置したので、Herokuでホスティングする場合はご活用ください。
環境変数を設定
以下の6項目を環境変数にセットします。ローカル検証時は.envファイルで設定してもokです
-
セサミ用
SESAME_UUID
... SESAMEデバイスを一意にするキー
SESAME_API_KEY
... SESAMEのWeb APIキー
KEY_SECRET_HEX
... SESAMEのシークレットキー
-> APIを使ってSesame4を操作する -
LINE用
CHANNEL_ACCESS_TOKEN
... チャネルアクセストークン。LINE Developersから取得
CHANNEL_SECRET
... チャネルシークレット。LINE Developersから取得
LINE_USER_ID
... チャネル内での自分のユーザーID。LINE Developersから取得
-> アクセストークンの発行・確認方法
LINE公式アカウントのWebhookを有効化する
フレックスメッセージにPostbackのボタンを設置しているため、Webhookの設定が必要です。
Postbackイベントを受け取るとアプリケーションはSESAMEに施錠コマンドを送ります。
スケジューラーにジョブを追加
Herokuスケジューラーで、一時間ごとに {アプリケーションURL}/remindme
のエンドポイントにcurlコマンドを実行する設定をします。
設定は以上です。
まとめ
使い始めて一週間くらい経ちましたが、Herokuの無料プランで問題なく運用できています。
少し修正してリッチメニューからカギを操作できるようにしても面白そうです。
SESAMEを使い始めてからカギを持ち歩くことがなくなり、QOLが上がりました。
迷われている方はぜひ買ってみてください!
ソースコード
ソースコードはGitHubに公開してます。