【Laravel・SwitchBot】たった1万円台で入室管理システムを導入する方法

こんにちは!
九保すこひです(フリーランスのITコンサルタント、エンジニア)

さてさて、AIによる仮想空間の支配がどんどん強まっている今日この頃です。
そんな時代の流れに乗っていかねばということで「デジタルとリアルの融合」について色々考えてました。

そうなると、やはり可能性を感じるのが・・・・・・

SwitchBot(スイッチボット)

です!

SwitchBotはウェブ上から物理操作ができるスグレモノですが、実は「指ロボット」だけでなく色々な機械を用意してくれています。

これは指ロボットです。

たとえば、次のような機械はアイデア次第で面白いことができそうじゃないですか?

  • 人感センサー
  • 開閉センサー
  • 水漏れセンサー
  • 温度・湿度計
  • 監視カメラ

そしてそんな中、ピックアップしたのが、

スマートロック(遠隔のドアキー操作)

です。

つまり!

今回のテーマ「スマートロックでQRコードをつかった入室システムをつくる」につながるわけです。

ということで今回は次のよう方に向けて記事を書いています。
ぜひ最後まで読んでくださいね!

  • 「Laravelをつかって入室管理システムをつくりたい」
  • 「1万円台で入室管理を設置したい」
  • 「AIの及びにくい、リアルの開発もやっておきたい」
  • 「デジタルとリアルの融合に興味がある」
  • 「スターウォーズみたくフォースで遠隔操作したい(笑)」

「子供の頃、フォースつかう
練習をしたことがあります!」

開発環境:Laravel 12.x + Vue 3(TypeScript) + Inertia

必要な機械のまとめ

今回は、ウェブ上からスマートロックを施錠、解錠します。
必要な機械は次の2つになります(合わせて1万円台で購入できます)

1. SwitchBot ロックLite8,980円(公式ページより抜粋2025/10/31)

※リンク先はSwitchBot ロックです。

Liteじゃなくて、ProとかノーマルでもOK!

2. SwitchBot ハブ ミニ5,480円(公式ページより抜粋2025/10/31)

ミニハブじゃなく、ノーマルのやつでもOK!

3. QRコードを読み取るデバイス

パソコンやスマホ、タブレットなど「ブラウザが起動できるもの」なら何でもOK!

開発の準備をする3ステップ

プログラムを書いていく前に、必要なものを用意しておきます。

1. 必要なパッケージをインストールする

JavaScriptでQRコードをつかうので、次のパッケージをインストールします。

npm i qrcode -D
npm i @types/qrcode -D

npm i jsqr -D

※2つ目はTypeScript用です。開発スピードが落ちるので、ホントはTypeScript使いたくないんですが、世間の流れに逆らえず・・・(笑)

2. SwitchBot APIのトークンを取得する

以前公開した記事にまとめていますので、以下記事の「SwitchBot APIのトークンを取得する」を参照して.envにセットしておいてください。

3. デバイスIDを取得する

SwitchBot APIをつかってスマートロックを操作するために「デバイスID」と呼ばれる固有のIDが必要になります。

このIDを取得しましょう。
まずは(説明書きを見ながら)SwitchBot Lock Liteをスマホでペアリングしてください。

次にホーム画面でペアリングした機械を選択。

右上にある歯車のマーク > Device Info」を選択。

BLE MAC」項目がデバイスIDになります。

では、取得したデバイスIDは.envにセットしておきましょう。

SWITCHBOT_LOCK_DEVICE_ID="(あなたのデバイスID)"

そして、コンフィグ登録もします。

// 省略

'switchbot' => [
'devices' => [
'lock' => env('SWITCHBOT_LOCK_DEVICE_ID'),
],
],

これでconfig('services.switchbot.devices.lock');でデバイスIDを取得できるようになりました。

SwitchBot APIにアクセスしやすくするため、Httpクライアントに専用macroをつくる

SwitchBot APIはcurlでアクセスできるのですが、以前公開した記事でもわかるとおり少し複雑な計算をしないといけません。

このコードを毎回書くのは面倒くさいですね。

なので、

Http::switchbot()->device()->command();

みたいなシンプルなコードでAPIを実行できるようにしておきましょう。
今回だけじゃなく、他の機能にも使えるので便利です!

app/Providers/AppServiceProvider.php

<?php

namespace App\Providers;

use Illuminate\Support\Facades\Vite;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Http;

class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}

/**
* Bootstrap any application services.
*/
public function boot(): void
{
Vite::prefetch(concurrency: 3);

// SwitchBotクライアント
Http::macro('switchbot', function () {

return new SwitchBotClient;

});

}
}

/* SwitchBotクラス */

class SwitchBotClient
{
public function device(string $deviceId): SwitchBotDeviceClient
{
return new SwitchBotDeviceClient($deviceId);
}
}

trait SwitchBotBaseTrait
{
// 共通
public function getHeaders(): array
{
$token = config('services.switchbot.access_token'); // あなたのアクセストークン
$secret = config('services.switchbot.secret_token'); // あなたのシークレットトークン

$nonce = $this->getGuid();
$t = time() * 1000;
$data = mb_convert_encoding($token . $t . $nonce, 'UTF-8');
$sign = hash_hmac('sha256', $data, $secret, true);
$sign = strtoupper(base64_encode($sign));

return [
'Content-Type: application/json; charset=utf8',
'Authorization: ' . $token,
'sign: ' . $sign,
'nonce: ' . $nonce,
't: ' . $t,
];
}

private function getGuid($data = null)
{
$data = $data ?? random_bytes(16);
assert(strlen($data) == 16);
$data[6] = chr(ord($data[6]) & 0x0f | 0x40);
$data[8] = chr(ord($data[8]) & 0x3f | 0x80);

return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
}
}

class SwitchBotDeviceClient
{
use SwitchBotBaseTrait;

public function __construct(private string $deviceId)
{
}

public function command(array $payload): array
{
$deviceId = $this->getDeviceId();
$headers = $this->getHeaders();
$url = 'https://api.switch-bot.com/v1.1/devices/' . $deviceId . '/commands';

$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);

if ($response === false) {

return ['message' => 'request_error'];

}

return json_decode($response, true); // レスポンスを配列として返す
}

public function status(): array
{
$deviceId = $this->getDeviceId();
$headers = $this->getHeaders();
$url = 'https://api.switch-bot.com/v1.1/devices/' . $deviceId . '/status';

$ch = curl_init($url);
curl_setopt($ch, CURLOPT_HTTPGET, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);

if ($response === false) {

return ['message' => 'request_error'];

}

return json_decode($response, true); // レスポンスを配列として返す
}

private function getDeviceId()
{
return str_replace(':', '', $this->deviceId);
}
}

SwitchBotBaseTraitの中で「アクセストークン」と「シークレットトークン」が取得できるようにconfig/services.phpに登録しておいてくださいね。

config/services.php

// 省略

'switchbot' => [
'access_token' => env('SWITCHBOT_ACCESS_TOKEN'),
'secret_token' => env('SWITCHBOT_SECRET_TOKEN'),
],

QRコードで解錠する部分をつくる3ステップ

では、ここからは本格的にLaravelの開発作業です。
必要ファイルをつくるので、以下コマンドを実行してください。

php artisan make:model LockPermission -msc

すると、「モデル」「マイグレーション」「Seeder」「コントローラー」が作成されます。
それぞれ中身を変更しましょう。

1. マイグレーションを変更する

DBテーブルの構造を決定します。

database/migrations/****_**_**_******_create_lock_permissions_table.php

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('lock_permissions', function (Blueprint $table) {
$table->id();
$table->uuid()->comment('UUID');
$table->string('device_id')->comment('デバイスID');
$table->dateTime('expires_at')->nullable()->comment('権限の有効期限');
$table->timestamps();
});

}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('lock_permissions');
}
};

2. Seederを変更する

テストデータをつくります。

database/seeders/LockPermissionSeeder.php

<?php

namespace Database\Seeders;

use App\Models\LockPermission;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Illuminate\Support\Str;

class LockPermissionSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$permissions = [
[
'device_id' => '(ここにあなたのデバイスID)',
'expires_at' => now()->addDays(30),
],
[
'device_id' => '(ここにあなたのデバイスID)',
'expires_at' => now()->subDays(30), // 期限切れ
],
[
'device_id' => '(ここにあなたのデバイスID)',
'expires_at' => null, // 永続的な権限(管理者権限など)
],
];

foreach ($permissions as $permission) {

$lockPermission = new LockPermission();
$lockPermission->uuid = Str::uuid();
$lockPermission->device_id = $permission['device_id'];
$lockPermission->expires_at = $permission['expires_at'];
$lockPermission->save();

}
}

}

3. コントローラーを変更する

最後にコントローラーです。

内容としては、

  • index:QRコード表示。テスト用。
  • createとstore:ロック解除

になっています。

app/Http/Controllers/LockPermissionController.php

<?php

namespace App\Http\Controllers;

use App\Models\LockPermission;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;

class LockPermissionController extends Controller
{
// QRコード表示
public function index()
{
// 期限切れも含めて全てのロック解除権限を取得
$lockPermissions = LockPermission::withoutGlobalScopes()->get();

return inertia('LockPermission/Index', [
'uuids' => $lockPermissions->pluck('uuid')
]);
}

// ロック解除(表示用)
public function create()
{
return inertia('LockPermission/Create');
}

// ロック解除(Switchbot API用)
public function store(LockPermission $lockPermission)
{
$status = 'FAILED'; // Enumなどで管理するのがベター

// SwitchBot APIでロック解除

$deviceId = config('services.switchbot.devices.lock');
$payload = [
'command' => 'unlock',
'parameter' => 'default',
'commandType' => 'command',
];
$response = Http::switchbot()
->device($deviceId)
->command($payload);

if($response['message'] === 'success') {

$status = 'SUCCESS';

}

// ログに解錠データを記録(本来はDBに保存した方がいいでしょう)
logger()->info('LockPermission used', [
'uuid' => $lockPermission->uuid,
'status' => $status,
]);

return ['status' => $status];
}
}

ビューをつくる

では、コントローラー内でセットしたビュー2つを用意しましょう。

1. Index

QRコードの一覧を表示するページです。
テスト用での表示ですので、本番では管理者用としてつかうことになるでしょう。

resources/js/Pages/LockPermission/Index.vue

<script setup lang="ts">
import { Head } from '@inertiajs/vue3';
import QRCode from 'qrcode';
import { nextTick, onMounted } from 'vue';

const props = defineProps<{
uuids: string[];
}>();

onMounted(() => {
nextTick(() => {
props.uuids.forEach((uuid) => {
const canvas = document.getElementById(
`canvas-${uuid}`,
) as HTMLCanvasElement | null;
if (canvas) {
QRCode.toCanvas(canvas, uuid, (error?: Error | null) => {
if (error) console.error(error);
});
}
});
});
});
</script>

<template>
<Head title="ロック解除UUID"></Head>

<div class="px-16 py-10">
<h1 class="mb-8 text-3xl font-bold">🔓ロック解除UUID</h1>
<div class="flex gap-5">
<div
v-for="uuid in uuids"
:key="uuid"
class="mb-10 rounded-lg bg-yellow-200 p-5 pb-4"
>
<canvas
:id="`canvas-${uuid}`"
class="mx-auto mb-5 h-20 w-20 rounded-sm"
></canvas>
<div
class="rounded-sm bg-yellow-400 px-3 py-1.5 text-xs font-bold text-yellow-800"
>
{{ uuid }}
</div>
</div>
</div>
</div>
</template>

表示するとこうなります。

※表示したページから、QRコードを右クリックするとダウンロードもできますよ👍

2. Create

QRコードを読み取り、その内容をデータ送信するページです。

resources/js/Pages/LockPermission/Create.vue

<script setup lang="ts">
import { Head } from '@inertiajs/vue3';
import axios from 'axios';
import jsQR from 'jsqr';
import { ref } from 'vue';

// QRコードスキャナー
const videoRef = ref<HTMLVideoElement | null>(document.createElement('video'));
const canvasRef = ref<HTMLCanvasElement | null>(null);
const isScanning = ref(false);
let stream: MediaStream | null = null;
const onScan = async () => {
isScanning.value = true;

// 10秒後に自動キャンセル
setTimeout(() => {
if (isScanning.value) {
onCancelScan();
}
}, 10000);

// カメラ起動
stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: 'environment' },
});
if (videoRef.value) {
videoRef.value.srcObject = stream;
videoRef.value.play();
}
scanLoop();
};
const scanLoop = () => {
if (!videoRef.value || !canvasRef.value) return;
const ctx = canvasRef.value.getContext('2d');
if (!ctx) return;

// 毎フレームチェック
const scanFrame = async () => {
if (videoRef.value?.readyState === videoRef.value?.HAVE_ENOUGH_DATA) {
if (!canvasRef.value || !videoRef.value) return;

// 画面幅に合わせて高さを調整
canvasRef.value.width = window.innerWidth;
canvasRef.value.height =
videoRef.value.videoHeight /
(videoRef.value.videoWidth / window.innerWidth);
// canvasにビデオ画面を描画
ctx.drawImage(
videoRef.value,
0,
0,
canvasRef.value.width,
canvasRef.value.height,
);
const imageData = ctx.getImageData(
0,
0,
canvasRef.value.width,
canvasRef.value.height,
);
const code = jsQR(
imageData.data,
imageData.width,
imageData.height,
);
if (code) {
const uuid = code.data;
onCancelScan();
onSubmit(uuid);
return;
}
}
requestAnimationFrame(scanFrame);
};
scanFrame();
};

// 送信(ロック解除)
type Status = 'IDLE' | 'SUCCESS' | 'FAILED';
const status = ref<Status>('IDLE');
const onSubmit = (uuid: string) => {
const url = route('lock_permission.store', { lockPermission: uuid });
axios
.post(url)
.then((response) => {
status.value = response.data.status;
})
.catch((error) => {
status.value = 'FAILED';
console.error('エラー:', error);
})
.finally(() => {
// 5秒後にステータスをリセット
setTimeout(() => {
status.value = 'IDLE';
}, 5000);
});
};
const onCancelScan = () => {
isScanning.value = false;
if (stream) {
stream.getTracks().forEach((track) => track.stop());
stream = null;
}
if (videoRef.value) {
videoRef.value.srcObject = null;
videoRef.value.pause();
}
};
</script>

<template>
<Head title="ロック解除"></Head>

<div class="px-8 py-10">
<h1 class="mb-8 text-center text-3xl font-bold md:text-4xl">
🔓QRコード<small>をスキャンして解錠してください</small>
</h1>

<div
class="text-center font-bold text-green-600"
v-if="status === 'SUCCESS'"
>
[完了]<br />
ロックが解除されました。
</div>
<div
class="text-center font-bold text-red-500"
v-if="status === 'FAILED'"
>
[エラー]<br />
一致するキーが存在しないか、有効期限が切れています。
</div>

<div class="fixed inset-0 z-10 bg-black/50" v-show="isScanning">
<div
class="fixed left-0 right-0 top-1/2 -translate-y-1/2 text-center"
>
<canvas ref="canvasRef"></canvas>
</div>
</div>

<div class="fixed bottom-12 left-0 right-0 text-center">
<button
type="button"
class="rounded-lg bg-sky-500 px-6 py-5 text-xl font-bold text-white"
@click="onScan"
>
QRコード<small>を読み取る</small>
</button>
</div>
</div>
</template>

見た目はこんなカンジです。

鍵が開いたままのときは、自動で施錠するようにする

ここまでは鍵を開ける動作でしたが、もしかすると鍵を閉め忘れる人がいるかもしれません。

もしくは「自動で閉めてくれよ…😅」という場合もあるでしょう。

ということで、タイマーで鍵の状態を監視し、もし開いたままになっていたら自動で施錠する機能もつくってみます。

では、専用のArtisanコマンドをつくるので、以下を実行してください。

php artsan make:command CheckSmartBotLockCommand

すると、ファイルが作成されるので中身をこのように変更します。

app/Console/Commands/CheckSmartBotLockCommand.php

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\Http;

class CheckSmartBotLockCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'check:switchbot-lock';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Check switchbot lock status and lock automatically if unlocked.';

/**
* Execute the console command.
*/
public function handle()
{
$deviceId = config('services.switchbot.devices.lock');

// ロックの状態を取得する
$response = Http::switchbot()
->device($deviceId)
->status();
$lockState = data_get($response, 'body.lockState', 'unknown');

if ($lockState === 'unlocked') {

// 空いたままなので、自動ロックする
$payload = [
'command' => 'lock',
'parameter' => 'default',
'commandType' => 'command',
];
Http::switchbot()
->device($deviceId)
->command($payload);

} else if (in_array($lockState, ['jammed', 'unknown'])) {

// ここは想定外なのでメール通知などをするといいでしょう。
logger()->warning('SwitchBot Lock in unexpected state', [
'lockState' => $lockState,
]);

}
}
}

※もしかすると、施錠とQRスキャンによる解錠がバッティングすることも考えられるので、解錠処理をしている場合は、自動施錠はしないようにしたほうがいいかもしれません(今回は省略しています)

※本来は開閉センサーをつかって厳密に施錠すべきですが、今回はテストということで追加しました。

おまけ:SwitchBotの機械をオークション買う場合は注意!(最悪ペアリングできないかも😫)

実は今回、SwitchBot Lock Liteをメルカリで購入したんですが、注意が必要です。

というのも、アプリのペアリングに失敗して「購入した証拠を出せ」と言われたからです。

レアケースかもですが…🤔

すると、以下のようにAmazonなどで購入したときのスクリーンショットをアップロードしないといけません。

本文:Please upload an image of your proof of purchase, such as an Amazon order screenshot or shopping receipt. The image must contain your order number and the product name.

日本語訳:購入証明書の画像をアップロードしてください。Amazonの注文画面のスクリーンショットやレシートなどをご利用いただけます。画像には注文番号と製品名が含まれている必要があります。

今回出品者さんが対応してくれて問題なかったんですが、中古の場合はそういったリスクもあるので注意しておいたほうがいいでしょう。

ちなみに、他の方が出品するSwitchBotの商品の説明に「ペアリングはすでに解除しています」と記述があったため、もしかすると元々のペアリングを解除すれば解決する可能性もあります。

テストしてみる

では実際にテストしてみましょう!

QRコードでロック解除するテスト

動画を用意したので、ぜひご覧ください。

うまくロック解除できましたね!

自動施錠する機能のテスト

では、閉め忘れたときの自動施錠です。
以下のコマンドを実行してみましょう。

どうなるでしょうか・・・・・・

はい!
こっちもうまくいきましたね。

すべて成功です😊✨

企業様へのご提案

今回のテクニックをつかうと、入室管理システムだけでなく次のような機能をつくることもできます。

  • 自動チェックイン・チェックアウト
  • QR認証でゲスト受付
  • レンタルスペースの自動貸し出し
  • 民泊のセルフ解錠
  • 資料室への期間を限定した入室
  • 宅配ボックスの自動受け取り
  • 保育園などの入退園管理

もしこういった機能をご希望でしたらお力になれるかと思いますので、ぜひお問い合わせからご相談ください。

お待ちしております😊✨

おわりに

ということで、今回はSwitchBot Lockをつかって入室システムをつくってみました。

個人的にSwitchBot「手頃な値段」で購入できるので、いいですね!

正直今回購入した機械は自分ではつかわない(つまり、ブログのためだけに買いました😂)ですが、リーズナブルな値段だと「研究」がしやすく、きっと裾野が広がりますね。

ちなみに過去にRFID(電波でICタグを読み書きする認識システム)の記事を書こうかとも思ったんですが、たしか機械が5万円とかするので見送ったことがあります。

いつかメルカリで安く売ってたら考えますね!(誰か投げ売りして🙏✨)

ということで、今回の作業も楽しかったです。
ぜひ皆さんもやってみてくださいね。

ではでは〜!

「たまに、目的の食材[だけ]を忘れて
帰ってくることがあるんですが・・・」

このエントリーをはてなブックマークに追加       follow us in feedly  
お問い合わせ、お待ちしております。
開発のご依頼はこちら: お問い合わせ
どうぞよろしくお願いいたします! by 九保すこひ