💭

Cloudflare Workers + HonoでSwitchBot APIから温湿度計のデータを定期的に取得する

2024/02/12に公開

やりたいこと

  • SwitchBot API から温湿度計のデータを定期的に取得して DB に保存する
  • 無料で実行する

なぜ Cloudflare Workers なのか

今まで無料で簡単なスクリプトの定期実行したい場合は Cloud Functions が自分の中で第 1 候補でした。
また、上記の要件を満たせるサービスは最近だと Vercel や Supabase, Deno などの各種プラットフォームが用意する Edge Functions でも十分満たせる範囲だと思います。
その中で Cloudflare Workers は下記の 2 点が圧倒的だったからです。
制限はありますが、その中で満たせるものに関しては極力 Cloudflare Workers を使用していきたいと考えています。

  • Cloudflare Workers のデプロイがとにかく早い
  • すぐ開発環境を用意できる

どのように実現したか

Cloudflare Workers には Cron Triggers という定期実行するための設定が可能です。
cron の設定やテストもとても楽にできます。
https://developers.cloudflare.com/workers/configuration/cron-triggers/

Cloudflare Workers 用のアプリケーションを作る際にはフレームワークとしていつも Hono を利用しています。より簡単に Cloudflare Workers のリソースを扱えるようになるのでとても便利です。
https://hono.dev/getting-started/cloudflare-workers

SwitchBot API をした温湿度計データの取得についてはすでにいくつもの記事が出ているので詳しく言及しません。

自分が書いた Cloudflare Workers + Hono のファイルを下記に載せておくので参考にしてみてください。

index.ts

import { PrismaClient } from "@prisma/client/edge";
import { withAccelerate } from "@prisma/extension-accelerate";
import { Hono } from "hono";

type Env = {
  SWITCH_BOT_TOKEN: string;
  SWITCH_BOT_SECRET: string;
  SWITCH_BOT_DEVICE_ID: string;
  DATABASE_URL: string;
};

type SwitchBotResponse = {
  statusCode: number;
  body: {
    deviceId: string;
    deviceType: string;
    hubDeviceId: string;
    humidity: number;
    temperature: number;
    version: string;
    battery: number;
  };
  message: string;
};

const app = new Hono<{ Bindings: Env }>();

async function generateSignature(token: string, secret: string) {
  const nonce = crypto.randomUUID(); // UUID v4を生成
  const t = Date.now(); // 現在のタイムスタンプ(ミリ秒)
  const stringToSign = `${token}${t}${nonce}`;
  const encoder = new TextEncoder();
  const secretEncoded = encoder.encode(secret);
  const stringToSignEncoded = encoder.encode(stringToSign);

  // HMAC-SHA256署名を生成
  const key = await crypto.subtle.importKey(
    "raw",
    secretEncoded,
    { name: "HMAC", hash: "SHA-256" },
    false,
    ["sign"]
  );
  const signArrayBuffer = await crypto.subtle.sign(
    "HMAC",
    key,
    stringToSignEncoded
  );
  const sign = btoa(String.fromCharCode(...new Uint8Array(signArrayBuffer)));

  return { sign, t, nonce };
}

async function fetchFromSwitchBot(
  token: string,
  deviceId: string,
  sign: string,
  t: number,
  nonce: string
) {
  // SwitchBot APIのエンドポイント
  const SWITCH_BOT_API_URL = `https://api.switch-bot.com/v1.1/devices/${deviceId}/status`;

  // APIヘッダーを設定
  const apiHeader = {
    Authorization: token,
    "Content-Type": "application/json",
    t: t.toString(),
    sign: sign,
    nonce: nonce,
  };

  // SwitchBot APIからデバイスリストを取得
  const response = await fetch(SWITCH_BOT_API_URL, {
    headers: apiHeader,
  });
  const res: SwitchBotResponse = await response.json();

  return res;
}

async function insertIntoDatabase(
  databaseUrl: string,
  humidity: number,
  temperature: number
) {
  const prisma = new PrismaClient({
    datasourceUrl: databaseUrl,
  }).$extends(withAccelerate());

  try {
    await prisma.<table_name>.create({
      data: <data>,
    });
  } finally {
    await prisma.$disconnect();
  }
}

const scheduled: ExportedHandlerScheduledHandler<Env> = async (
  event,
  env,
  ctx
) => {
  const token = env.SWITCH_BOT_TOKEN;
  const secret = env.SWITCH_BOT_SECRET;
  const deviceId = env.SWITCH_BOT_DEVICE_ID;
  const databaseUrl = env.DATABASE_URL;

  const { sign, t, nonce } = await generateSignature(token, secret);
  const res = await fetchFromSwitchBot(token, deviceId, sign, t, nonce);
  await insertIntoDatabase(
    databaseUrl,
    res.body.humidity,
    res.body.temperature
  );
};

export default {
  fetch: app.fetch,
  scheduled,
};

データベース関連の記述に関しては自身が使用したいサービスによって適宜変更してください。
自分は TiDB Serverless にデータを保存しています。

Cloudflare Workers から TiDB Serverless への接続に関しては @tidbcloud/serverless を使用した方法が公式のドキュメントにリンクとして張られています。
https://github.com/tidbcloud/serverless-js

ただ、まだ beta 版だからなのかこの方法だと本番環境のみでたまにエラーが出てしまったので Prisma での接続方法に変更しています。
https://www.prisma.io/docs/orm/prisma-client/deployment/edge/deploy-to-cloudflare-workers

何かの参考になれば幸いです。

Discussion