エンジニアリング

LINEのbotを使って家庭の家事業務を円滑にすることをGCPのCloud Functions を使ってやってみた(python3)

どーもヘタレエンジニアまる です。

我が家は共働き家庭でして家事分担でよく揉めます、ケンカになることもしばしば。

ということでお互いが何をやったかの可視化を目的に家事botをLineで作ることにしました。

DIYの第1弾は監視カメラを作りました、興味があればそちらもどうぞ。

Raspberry Piを使った格安監視カメラシステムを作った!実家に帰省してます。今回は片手に「Raspberry Pi」を携えております・・・・・ 田舎の古民家に監視カメラを設置しよう 実...

第二弾は手抜きシステム

自分が投資したい株式投資銘柄をスクリーニングツールから抽出→メール通知させてみた。手抜き感炸裂のseleniumとjenkinsで!どーもヘタレエンジニア まる です。 最近スノーボードなどでお金を使いすぎていることもありちょっと財テクを日曜大工的にやってみよう第2弾...

ルンバさんハックはこんなこともやってます

(へたれエンジニアの引っ越し)新しいラグを買ったらルンバが動かなくなった・・・引っ越しシリーズです (へたれエンジニアの引っ越し)物件を探す時に面倒なのでチャット不動産を試してみた (へたれエンジニアの引っ...

システム概要

実際の出来上がったブツはこんな感じです

こんな感じで朝6:50に今日やらないといけない家事をbotからメッセージとして発信されます。(cron-job → LINE1)

で、「やるよ」「やらないよ」を選びます。

そうするとデータが裏で登録されます(LINE→LINE3)

やらないよを選んだ場合、昼13:00ごろに再び聞かれます。「やるよ」を選んでいた場合は「本日の家事は完了してます」となります。(cron-job → LINE1)

※これは嫁が夜勤の日があるのでその分担です。

毎週土曜日にはこんな感じでやったこと・やれていないことがリストとしてメッセージが発信されてその週に出来ていない家事を一気にやります。(cron-job → LINE2)

まとめるとシステム構成はこんな感じです。

botの登録をしましょう

まずはbotの登録のためにLINEのDeveloper登録をしましょう

登録できたら新規プロバイダーを作成します

今回はMessageAPIを使います

プロバイダ > 新規チャンネルの作成

適当にもろもろ埋めていきます

こんな感じで出来ました

ここで作ったチャンネルのChannel Secret と アクセストークンを確認します

LINEにアクセスするための情報はこれでいったんOK、次に関数を作ります。

DataStore を利用できるようにしましょ

今回家事やったよの情報をDataStoreに保存してみることにしました。(初めて使う)

とりあえず雑にやってみます。

エンティティを新規に作成

こんな感じで定義して1レコード作っておきます、これでデータ準備は完了

Cloud Functions を利用できるようにしましょ

最終的にはこんな感じでCloud Functions に関数を登録していく

5分でわかる!Google Cloud Functions の使い方(あぱーブログさん) がめちゃめちゃわかりやすかったので参考にして登録させてもらいました。

まずはLINE1のプログラム(毎日通知する)を書きましょう

line-function-01

#main.py
import os
import base64, hashlib, hmac
import logging
import datetime

from flask import abort, jsonify

from linebot import (
    LineBotApi, WebhookParser
)
from linebot.exceptions import (
    InvalidSignatureError
)
from linebot.models import (
    MessageEvent, TextMessage, MessageAction, TemplateSendMessage,
    ButtonsTemplate, TextSendMessage, URIAction
)

from google.cloud import datastore

#これで基本的に自動通知用の関数として使う、こちらで定期通知イベント発火をすることとする
LINE_CHANNEL_ACCESS_TOKEN = "LINEのチャンネルアクセストークン"
LINE_CHANNEL_SECRET = "LINEのチャンネルシークレット"
line_bot_api = LineBotApi(LINE_CHANNEL_ACCESS_TOKEN)
client = datastore.Client("GCPのPJ_ID")

def main(req):
    user_id = ["LINEに通知するユーザーのuserID1","LINEに通知するユーザーのuserID2"]
    weekday = datetime.date.today().weekday()
    #0=月曜日
    weekday_list = {
        0:"月曜日",
        1:"火曜日",
        2:"水曜日",
        3:"木曜日",
        4:"金曜日"
    }
    task_list = {
        0:"ルンバの掃除オンにして家出てね!",
        1:"ごみ捨てをして行ってね!★",
        2:"洗濯してね!",
        3:"プラスチックごみとカン・ビン・ゴミ捨てていってね!",
        4:"洗濯取り込んでね!"
    }
    task_image_list = {
        0:"https://1.bp.blogspot.com/-huCenw23rLw/Ur1GQELz2mI/AAAAAAAAcaQ/799-0K2TH1E/s400/robot_soujiki.png",
        1:"https://1.bp.blogspot.com/-3RwkghjMBIA/W0mF0B6FO2I/AAAAAAABNU0/LqvuIulVyog7jzDiEdtADnAJiWq2h6RRACLcBGAs/s800/gomidashi_man.png",
        2:"https://2.bp.blogspot.com/-RgdKG023QZc/W0mF3APVtRI/AAAAAAABNVI/cH8nmRPN5KA-pfMTOZjJC6isMXAECt6egCLcBGAs/s800/jiko_sentakuki_rouden_earth.png",
        3:"https://2.bp.blogspot.com/-xhHFpqit9Ds/WdyDbw9vAfI/AAAAAAABHco/EXUKorrGnqE46L3-Ac7OWBu-gdyNbhtSQCLcBGAs/s400/gomi_petbottle_fukuro.png",
        4:"https://2.bp.blogspot.com/-oW0tc6eL_98/UrEhmZgntFI/AAAAAAAAb64/IvZPKjIju6c/s450/sentaku_tatamu.png"
    }


    #メッセージ作成
    url = "https://このあと作る登録用のfunction URL/hogehoge?&question=" + task_list[weekday] + "&date=" + datetime.date.today().strftime('%Y-%m-%d')


    messages = TemplateSendMessage(
        alt_text="今日の家事は・・・・?",
        template=ButtonsTemplate(
            text=task_list[weekday],
            title="今日は"+weekday_list[weekday]+"! 本日の家事は・・・?",
            image_size="cover",
            thumbnail_image_url=task_image_list[weekday],
            actions=[
                URIAction(
                    uri=url+"&member=" + user_id[0] + "&answer=1",
                    label="はーい!XXがやります!"
        		),
                URIAction(
                    uri=url+"&member=" + user_id + "&answer=1",
                    label="はーい!XXがやります!"
        		),
                URIAction(
                    uri=url+"&member=" + user_id[0] + "&answer=0",
                    label="すまぬ!XXは本日は出来ぬ!"
        		),
                URIAction(
                    uri=url+"&member=" + user_id + "&answer=0",
                    label="すまぬ!XXは本日は出来ぬ!"
        		)
            ]
        )
    )
    #本日すでに家事実行していたら、実行済のメッセージを送る
    key = client.key("Task")
    query = client.query(kind="Task")
    dt = datetime.datetime.now()
    dt = dt.replace(hour=0, minute=0, second=0, microsecond=0) # Returns a copy
    query.add_filter('insert_day', '>=', dt)
    for item in query.fetch():
        if "1" in item['task_execute']:
            messages = TextSendMessage(text=f"本日の家事は完了してます")

    line_bot_api.multicast(user_id, messages=messages)


if	__name__ == "__main__":
    main()
#requirement.txt

# Function dependencies, for example:
# package>=version
flask
line-bot-sdk
google-cloud-datastore

ここの通知するためのユーザーIDは別途取得する必要があります。

自分のユーザーIDはLINE Developersで確認できます

奥さんのLINE IDは同じようにdevelopersに登録して確認してもらうか、このへんのAPIを呼んで確認するbotを別途作って取得する必要があります。

ユーザーID取得するためのメッセージに反応するbotを別途作ってみましょう

こんな感じ

line-function-getUserId

import os
import base64, hashlib, hmac
import logging

from flask import abort, jsonify

from linebot import (
    LineBotApi, WebhookParser, WebhookHandler
)
from linebot.exceptions import (
    InvalidSignatureError
)
from linebot.models import (
    MessageEvent, TextMessage, MessageAction, TemplateSendMessage,
    ButtonsTemplate, TextSendMessage
)

import datetime


#ユーザーのメッセージに反応する関数
def main(request):

    channel_secret = os.environ.get('LINE_CHANNEL_SECRET')
    channel_access_token = os.environ.get('LINE_CHANNEL_ACCESS_TOKEN')

    line_bot_api = LineBotApi(channel_access_token)
    parser = WebhookParser(channel_secret)

    body = request.get_data(as_text=True)
    hash = hmac.new(channel_secret.encode('utf-8'),
        body.encode('utf-8'), hashlib.sha256).digest()
    signature = base64.b64encode(hash).decode()

    if	signature != request.headers['X_LINE_SIGNATURE']:
        return abort(405)
    try:
        events = parser.parse(body, signature)
    except InvalidSignatureError:
        return abort(405)
    
    for event in events:
        if not isinstance(event, MessageEvent):
            continue
        if not isinstance(event.message, TextMessage):
            continue
    #通常リプライ
    profile = line_bot_api.get_profile(event.source.user_id)
    user_id = ["自分のuserID","他に通知したい人がいればここに"]
    messages=TextSendMessage(text=event.source.user_id)
    line_bot_api.multicast(user_id, messages=messages)
    return jsonify({ 'message': 'ok'})
# Function dependencies, for example:
# package>=version
line-bot-sdk
flask

こいつを作ったあとトリガーのURLをLINEのWebhookに設定する

これで該当チャンネルで呟けばUserIDが取得できます。

このコードをいじくればなにかつぶやいたらそれに連動してなにかするようなシステムを組めますが、今回はあくまでもユーザーID取得のためのものですので確認した以降は使いません。

家事をやる・やらないをアクションした後通知する部分を作りましょう

続いて やります! やりません!をアクションした後のbotの通知を作りましょう。

こちらはつぶやいたURLにパラメータをつけて渡す形で実現します

line-function-01の以下URL作成部分で渡すURLを構築してます

question = 家事名

date = 日付

member = 自分 or 嫁

answer = 0 やる 1やらない

こんな感じのパラメータを渡して家事登録をします

“https://このあと作る登録用のfunction URL/hogehoge?&question= 家事名 + “&date=” + datetime.date.today().strftime(‘%Y-%m-%d’)&member=” + 自分 or 嫁 + “&answer= 0 やる 1やらない”,

line-function-03

import os
import base64, hashlib, hmac
import logging
import datetime
import urllib.parse


from flask import abort, jsonify

from linebot import (
    LineBotApi, WebhookParser
)
from linebot.exceptions import (
    InvalidSignatureError
)
from linebot.models import (
    MessageEvent, TextMessage, MessageAction, TemplateSendMessage,
    ButtonsTemplate, TextSendMessage, URIAction
)

from google.cloud import datastore


#これで基本的に自動通知用の関数として使う、こちらで定期通知イベント発火をすることとする
LINE_CHANNEL_ACCESS_TOKEN = "LINEのチャンネルアクセストークン"
line_bot_api = LineBotApi(LINE_CHANNEL_ACCESS_TOKEN)
client = datastore.Client("GCPのPJ名")

def main(request):
    """
    	?member=LineユーザーID&question=質問&answer=0:No1:yesで設定&date
    """

    request_json = request.get_json()
    user_id = ["Lineの通知したいuserID","Lineの通したいuserID"]

    if request.args and 'member' in request.args:
        member = request.args.get('member')
    if request.args and 'question' in request.args:
        question = request.args.get('question')
    if request.args and 'answer' in request.args:
        answer = request.args.get('answer')
    if request.args and 'date' in request.args:
        date = request.args.get('date')
    if "LINE USER ID" in member:
        member_name = "XX"
    else:
        member_name = "XX"
    return_text =  member_name + "は今日の" + question + "を"
    if "1" in answer:
        return_text += "やります"
    else:
        return_text += "すまぬ・・・できません"

    messages = TextSendMessage(text=return_text)
    line_bot_api.multicast(user_id, messages=messages)
    data_insert(member, question, answer, datetime.datetime.strptime(date, "%Y-%m-%d"))


    return "<h1>返信が完了しましたブラウザを閉じてください</h1>"

def data_insert(member, question, answer, date):
    # クライアントの設定
    key = client.key("Task")

    entityA = datastore.Entity(key)
    dataA = dict()
    dataA['kaji_naiyo']= question
    dataA['task_execute']= answer
    dataA['insert_day']= date
    dataA['member']= member
    entityA.update(dataA)

    client.put_multi([entityA])
# Function dependencies, for example:
# package>=version
flask
line-bot-sdk
google-cloud-datastore

これで選択肢が出た後のアクションをDataStoreに登録することが出来ました

土曜の家事実施状況通知

最後に1週間のタスク実施状況を出力する関数を作りましょう

line-function-02

import os
import base64, hashlib, hmac
import logging
import datetime
from datetime import timedelta

from flask import abort, jsonify

from linebot import (
    LineBotApi, WebhookParser
)
from linebot.exceptions import (
    InvalidSignatureError
)
from linebot.models import (
    MessageEvent, TextMessage, MessageAction, TemplateSendMessage,
    ButtonsTemplate, TextSendMessage, URIAction
)

from google.cloud import datastore

#これで基本的に自動通知用の関数として使う、こちらで定期通知イベント発火をすることとする
LINE_CHANNEL_ACCESS_TOKEN = "LINEのチャンネルアクセストークン"
line_bot_api = LineBotApi(LINE_CHANNEL_ACCESS_TOKEN)
client = datastore.Client("GCPのプロジェクトID")

def main(req):
    user_id = ["ユーザーID","ユーザーID"]
    weekday = datetime.date.today().weekday()
    #0=月曜日
    weekday_list = {
        0:"月曜日",
        1:"火曜日",
        2:"水曜日",
        3:"木曜日",
        4:"金曜日"
    }


    #今週の条件 土曜日発火のため5日前の月曜日からの未実行データを抽出
    key = client.key("Task")
    query = client.query(kind="Task")
    dt = datetime.datetime.now()
    dt = dt.replace(hour=0, minute=0, second=0, microsecond=0) # Returns a copy
    before_dt = dt - timedelta(days=5)

    query.add_filter('insert_day', '>=', before_dt)
    query.add_filter('insert_day', '<=', dt)
    #query.order["insert_day"]

    messages = "今週の家事実施状況は以下の通りです(土曜定期配信)\n"
    for item in query.fetch():
        messages = messages + item['insert_day'].strftime('%Y-%m-%d')
        messages = messages + ":" + item['kaji_naiyo'] + '-'
        if item['task_execute'] == '0':
            messages = messages + "未実施"
        else:
            messages = messages + "実施済"
        if item['member'] in user_id[0]:
            messages = messages + "(XX)\n"
        else:
            messages = messages + "(XX)\n"

    line_bot_api.multicast(user_id,TextSendMessage(text=messages))



if	__name__ == "__main__":
    main()
# Function dependencies, for example:
# package>=version
flask
line-bot-sdk
google-cloud-datastore

これで今週誰がやる・やらないを選択したかの一覧が通知されます

すべての関数をcron登録をする

最後に cron-job.org で定期実行をするjobを登録します

これでシステムが出来ました。

運用してからの感想

今の所、なかなか順調に運用できてます。

嫁も通知がきて何も考えないで家事のやった、やれなかったが返信出来るからストレス少ないというフィードバックをもらっているのでしばらくこのシステムを使って家事分担をしてみます。

※その先にはどっちが家事やっているかの可視化が出来ることは内緒です・・・・笑