🏡

シェアハウスの掃除当番と誕生日とゴミの日をBot化した話

2024/12/15に公開

1. はじめに

シェアハウスでの生活は、異なるライフスタイルを持つ人々との共同生活ならではの課題があります
そうです 同居人の誕生日を忘れてしまったときのあの気まずさです
誕生日をうっかり忘れてしまい、コミュニティの雰囲気が悪化した経験がある方もいらっしゃるのではないでしょうか
半分冗談ですが、きっと経験ある方もいらっしゃることでしょう!

あとゴミの日を忘れてしまってゴミが放置されてしまう問題もありますね

誕生日についての問題はさておき、様々なシェアハウスで過ごしてきた経験、遊びに行った経験から、掃除当番やゴミの日について抱える課題はどこも同じだなと思いました
掃除当番については、ホワイトボードで書いておいて、入居者が入れ替わったときにはまたシフトを組み直して…など結構な時間がこの作業に割かれていることでしょう
ゴミの日については言わずもがなですが、地域によって細かなルールの違いがあることから、把握していない人が間違えてゴミを出してしまって、ゴミ収集の人に起こられて、ゴミがそのままになってしまうことがあると思います

四六時中何もかも自動化して効率化を図りたいと考えている効率厨エンジニアとしては、これらの課題を解決するためにボットによる自動化システムの開発に着手しました
このシェアハウスではコミュニケーションツールとしてSlackを使っていたので、Slackのボットを導入しました

2. システム概要

実装した機能としては以下になります

1. 掃除当番Bot
    - 自動ローテーション管理
    - リマインド通知
    - 掃除する場所の説明
2. ゴミ出しBot
    - 収集日前日の通知
    - ゴミの種類別案内
3. 誕生日Bot
    - 自動お祝いメッセージ

技術スタック

  • プラットフォーム: Slack Incoming Hook
  • バックエンド: GAS
  • データベース: Spreadsheet

GASのファイル構成は以下です

  • celebration.gs
  • cleanDuty.gs
  • takeOutGarbage.gs
  • utils.gs

他のファイルの内容は後で紹介しますが、utils.gsは以下のとおりです

// slackにメッセージを投稿する共通関数
const notificationToSlack = (message, url) => {
  const jsonData = { "text": message }
  const payload = JSON.stringify(jsonData)
  const options =
  {
    "method": "post",
    "contentType": "application/json",
    "payload": payload
  }
  UrlFetchApp.fetch(url, options)
}

const sheet = SpreadsheetApp.getActiveSpreadsheet()
// シートの名前で取得する
const birthdaySheet = sheet.getSheetByName("誕生日Bot管理シート")
const cleanDutySheet = sheet.getSheetByName("掃除当番Bot管理シート")

const URL = {
  cleanNotifyUrl: "https://hooks.slack.com/services/ZZZ/YYY/XXX",
  birthdayCelebrationUrl: "https://hooks.slack.com/services/ZZZ/YYY/XXX",
  takingOutOfGarbageUrl: "https://hooks.slack.com/services/ZZZ/YYY/XXX",
  // テスト用Incoming hooks
  dmXXX: "https://hooks.slack.com/services/ZZZ/YYY/XXX"
}

なお、GASの初期設定といいますか、使い方については以下の記事を参考にしてみてください

Google Apps Script(GAS)の基本的な使い方|Hello Worldを解説!

incoming hooksのURLは@hayato94087さんのServer Actionsを利用して、Slackのチャンネルに通知を飛ばすの記事で紹介されている「Incoming Webhooksの設定手順」で取得してください

3. 掃除当番Bot

まず掃除当番は、SpreadSheetでは以下のように管理しています
シート名と内部でのコードが密接に関係しているので、動かす場合は合わせてください

実際のコードは以下です

const cleanDuty = () => {
  // 最後のデータが存在する行を取得 
  // getLastRowはシート全体の値がある最後の行になってしまうので以下の方法で取得
  const lastRow = cleanDutySheet.getRange("A:A")
    .getValues()
    .filter(String) // 空の値を除外
    .length

  // 2からになっているのはヘッダー部分は取得しないから
  const members = cleanDutySheet
    .getRange(`A2:C${lastRow}`)
    .getValues()
    .map(data => ({
      name: data[0],
      id: data[1],
      onDuty: data[2] === '◯'
    }))

  // 現在の掃除当番を取得
  const currentDutyMemberIndex = members.findIndex((member) => member.onDuty)
  if (currentDutyMemberIndex === -1) return console.error('err') // debug 丸がついていなかった場合
  const nextDutyMemberIndexx = currentDutyMemberIndex === members.length - 1 ? 0 : currentDutyMemberIndex + 1
  const nextDutyMember = members[nextDutyMemberIndexx]

  // メッセージ内容があるセルの中身を取得
  const msgContent = cleanDutySheet.getRange("D11").getValue();

  const message = `
今週の掃除当番は<@${nextDutyMember.id}>です。
${msgContent}
`
  notificationToSlack(message, URL.cleanNotifyUrl)
  // notificationToSlack(message, URL.dmXXX) // テストしたいときにこっちにする

  // === 掃除当番を通知したら掃除当番の「◯」をその人に付け替える処理 ===
  const sheet = cleanDutySheet.getDataRange();
  const values = sheet.getValues();

  // 現在の当番から「◯」を削除
  values[currentDutyMemberIndex + 1][2] = ''; // +1 はヘッダー行の分
  // 次の当番に「◯」を付ける
  values[nextDutyMemberIndexx + 1][2] = '◯'; // +1 はヘッダー行の分
  // 更新したデータをシートに書き戻す
  sheet.setValues(values);
  // debug
  console.log(`掃除当番を ${members[currentDutyMemberIndex].name} から ${nextDutyMember.name} に更新しました。`);
}

こちらのコードを設定したうえで、毎週どの曜日、どの時間にこれを実行してリマインドするのか、という設定をGASの中で設定します
左のメニューで、下から2つ目のストップウォッチのマークを選択すると、以下のような画面になります

そして右側の鉛筆のマークから編集画面を出して、以下の画面で、通知したい時間を設定します

これで設定した時間に通知が来るようになります

シンプルな作りですが、自動化することによって掃除当番について能動的に確認しなくても受動的に確認できるようになるのは楽です

このシェアハウスでは1週間に1回のペースで掃除当番を回しているのですが、人によってはこの週は忙しいから次の人と変わってほしい、などはコミュニケーションしながらやっています
そのうちそこもシステム化したいところですね

また、今はスラックのIDと名前を手作業で入力していますが、入居者用のチャンネルで掃除当番を回しているので、手作業でメンバーのSlack IDや名前を入力しなくてもいいようにそこも自動化したい思いはありますね

掃除当番はこんな感じです

4. 誕生日Bot

次に誕生日ボットについてです

先程のスプレッドシート内で、別のシートで管理しています

コードは以下

const celebration = () => {
  const sheetData = birthdaySheet.getDataRange().getValues()
  const header = sheetData[0]

  // 今日の日付を取得
  const today = Utilities.formatDate(new Date(), 'Asia/Tokyo', 'MM/dd')

  // 今日の日付と同じ誕生日の人を取得
  const birthDayMemberNames = sheetData.filter(data => {
    const birthdayColumnValue = data[header.indexOf('誕生日')]
    if (!birthdayColumnValue) return false

    const birday = Utilities.formatDate(new Date(birthdayColumnValue), 'Asia/Tokyo', 'MM/dd')
    return birday === today
  }).map(data => `${data[header.indexOf('名前')]}さん`)

  if (birthDayMemberNames.length === 0) return // 誕生日の人がいる場合だけ処理を継続させる

  const message = `今日は、:birthday: ${birthDayMemberNames.join(', ')}:birthday: のお誕生日です!:clap:` + '\n\n' + 'おめでとうございます!良い一年を!!:smile:'

  notificationToSlack(message, URL.birthdayCelebrationUrl)
}

シンプルな作りですね

こちらも先程と同様にスケジューラーを組んで実行しています。
毎日実行しておくようにして、誕生日の人がいたら以下のように投稿される仕組みです

先程の掃除Botの方はメッセージを変えたい頻度が高かったためにスプレッドシート内部で定義していましたが、こちらのBotはそうそう変えることがない上に、メッセージ自体はさほど重要ではないので固定にしています

5. ゴミ出しBot

ゴミ出しの方も同じような仕組みでやっています

ゴミ出しの内容は基本的に変わることがないため、スプレッドシートでは管理しておらず、コード内部で直接書いています

const obj = {
  resource: "明日は水曜日: *資源ごみ* の日です!以下のゴミをまとめて出しましょう!"
    + '\n'
    + '• 古紙(新聞、雑誌類、紙パック、ダンボール)'
    + '\n'
    + '• ガラスびん(飲み物や食品のガラスびん)'
    + '\n'
    + '• 缶(飲み物や食品のアルミ缶スチール缶)',
  combustible: '明日は火・金曜日: *可燃ごみ* の日です!以下のゴミをまとめて出しましょう!'
    + '\n'
    + '生ゴミ、少量の枝葉、紙くず、プラスチック類、ゴム・皮革製品、衣類 等',
  incombustible: '明日は第1・3木曜日: *不燃ごみ* の日です!以下のゴミをまとめて出しましょう!'
    + '\n'
    + '金属、ガラス製品、陶磁器類、使い切ったスプレー缶、ライター、一辺の長さが30cm以下の小型家電製品 等',
  plasticBottle: '明日は第2・4木曜日: *ペットボトル* の日です!ペットボトルをまとめて出しましょう!'
    + '\n'
    + 'キャップ・ラベルを外して軽くすすぎ、つぶしてお出しください。'
}

const takingOutOfGarbage = () => {
  const d = new Date()
  const tmr = new Date(d.setDate(d.getDate() + 1))
  const tmrWeek = tmr.getDay()
  switch (tmrWeek) {
    // // 火曜日と金曜日:可燃ごみ
    // 可燃ごみはゴミステーションがあり、いつでも捨てられるために、通知しないことにした 2024/01/02時点
    // case 2:
    // case 5:
    //   notificationToSlack(obj.combustible)
    //   break
    case 3:
      // 水曜日:資源ごみ
      notificationToSlack(obj.resource, URL.takingOutOfGarbageUrl)
      break
    case 4:
      const tmrWeekNumber = Math.ceil(tmr.getDate() / 7)
      if ([1, 3].includes(tmrWeekNumber)) {
        // 第1・3木曜日:不燃ごみの日
        notificationToSlack(obj.incombustible, URL.takingOutOfGarbageUrl)
      } else {
        // 第2・4木曜日:ペットボトルの日
        notificationToSlack(obj.plasticBottle, URL.takingOutOfGarbageUrl)
      }
      break
  }
}

こちらも毎日実行しておくようにスケジューラーを設定しておき、ロジックとしては翌日が何曜日、第1,2,3,4の何曜日なのかを判定して、一致したらメッセージを投稿しておく仕組みです

当日の朝だと時間がなかったりして不都合なケースが多かったので、前日の夜に判定して投稿される仕組みにしました

6. 導入効果

これらのBotを導入することで

  • 誕生日の人のことをちゃんと祝う文化が定着した
  • 誕生日の人がいたらご飯食べに行ったりしてコミュニケーションが促進された
  • 掃除当番がメンションされることによってシェアハウスがきれいに保たれるようになり、掃除当番の設定責務から開放された
  • ゴミ出しが前日に通知されることによってゴミが放置されることが激減した

7. 今後の展望

今はスプレッドシートで管理していますが、どうしてもUI/UXが良くないので、ここらへんを改善できるようにしたいなと思います
アイデアはありません

また、スマートキー導入やその他のIT化できる部分があれば積極的に取り組みたいなと思います(オーナーが許せば

8. まとめ

エンジニアとしての知見を駆使して、みんなの時間を奪っていた面倒くさい業務を削減できた&誕生日Botでコミュニケーション促進できたことは喜ばしいなと思います

今回アドベントカレンダーということで、頑張って記事を書いてみました
シェアハウスの他メンバー、関わってくれている方の記事も乞うご期待!

まちのばアドベントカレンダー2024

出典:

Discussion