5
Help us understand the problem. What are the problem?

posted at

updated at

カギの閉め忘れを見張ってくれるLINEボットを作った【SESAME 4】

作った背景

私自身が忘れっぽい性格なので、たまに自宅のカギを掛けずに出かけてしまうことがあります。
これを解決するため、カギの状態を監視して開いたままのときはLINE通知するアプリケーションを作ってみました。

成果物イメージ

一定時間ごとに自宅のカギの状態を監視して、開いていれば施錠を促すLINEを送ります。
Node.jsのアプリケーションをHerokuにデプロイします。
image.png

用意したもの

SESAMEをAPIから操作する方法

SESAMEではWeb APIが無料で提供されていて、自作プログラムから施錠解錠することができます。
詳しい使い方やセットアップの方法は以下の記事にまとめてますのでご参照ください。

Node(Express)でWebサーバーを構築

index.js

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

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

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に施錠コマンドを送ります。

image.png

スケジューラーにジョブを追加

Herokuスケジューラーで、一時間ごとに {アプリケーションURL}/remindme のエンドポイントにcurlコマンドを実行する設定をします。
image.png

image.png

設定は以上です。

まとめ

使い始めて一週間くらい経ちましたが、Herokuの無料プランで問題なく運用できています。
少し修正してリッチメニューからカギを操作できるようにしても面白そうです。

SESAMEを使い始めてからカギを持ち歩くことがなくなり、QOLが上がりました。
迷われている方はぜひ買ってみてください!

ソースコード

ソースコードはGitHubに公開してます。

参考ページ

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
5
Help us understand the problem. What are the problem?