🐕

Raspberry Pi Zeroで実家の長寿犬のために温度をモニター with LINE Messaging API & GAS

2024/10/25に公開

はじめまして!記念すべき(個人としての)投稿第一弾は,Raspberry Pi Zeroを使った温度モニタリングシステムにすることにしました.本記事ではラズパイのセットアップからLINE botを用いたモニタリング機能,そして運用方法まで,全ての情報を共有したいと思います.結構長くなりますので,技術部分はいくつかの記事に分けています.この記事では,全体の流れを共有できればと思います.

突然ですが,実家の犬は御歳18歳,認知症に加えて足腰も弱くなっており,平日の昼は部屋にある柵の中で留守番の任務を全うしています.
しかし今年の夏,なかなかな猛暑でした... 油断してると部屋の中がすぐにサウナになってしまいます.
母は,お留守番をする犬のために,この夏は部屋のエアコンを常につけておくことを決めました.
しかし,心配するのは,「故障や入れ忘れで部屋の温度が上がってしまったら...」

ということで,はじめて自分でラズパイを購入して,温度モニターを作ろう,というわけです.今回こだわった部分は,遠方にある実家のために,コンセントを刺すだけで起動できる,遠方からも(Google Spreadsheetなどを使って)様々な微調整ができる,という部分です.

通知イメージ

「はじめて」から行ったからこその,懇切丁寧な記事を目指して書いていきますのでよろしくお願いします.
参考にしたサイトは随時記事内に示し,まとめて記事の一番下にも再掲いたします.


本記事およびそのサブ記事に書いてあること

  • 使用した機器・物品 (購入当時の金額・購入サイトへのリンクあり)
  • ラズパイのセットアップ (OSの書き込み, SSH, Wifiの設定など)
  • 温度センサ「DHT11」モジュールの基本的な使い方
  • Google Apps Script を使った LINE BotやSpreadsheetの運用
  • LINE Messaging API のセットアップと基本的な使い方 (Reply, Push)
  • Google Cloud の Sheet APIを使ったPythonとSpreadsheetのやりとり
  • ラズパイの起動時にプログラムを実行する方法
  • タクトスイッチを用いてラズパイのシャットダウンボタンを作る方法

おことわり

書く場所どこにしようか迷ったけど一旦ここで.

  • 本記事は,あくまでも初心者が書いているため,責任は取れません.(公式ドキュメントや色々なサイト・記事を参考にしてファクトチェックを行いながらお願いします)
  • 間違っているところなどはぜひ教えていただけると嬉しいです.
  • 画面のキャプチャなどは macOS のものとなります.

全体のイメージ


全体図

ラズパイを買おう

まずは,ラズパイを買わなきゃいけません.今回は,温度をモニタリングして,いくつかAPIを叩く,という程度ですので,無線通信機能がついているかつ,値段がお手頃なRaspberry Pi Zeroを購入することにしました.Raspberry Piの正規代理店であるRaspberry Pi Shop by KSY というサイトで探してみることにしました.

今回実際に購入したのは,以下のものになります.

  • Raspberry Pi Zero WH
    • 2970円 (記事執筆当時)
    • GPIOピンヘッダーがすでについている... ということでこちらの商品を選びました
  • 温湿度センサーモジュールDHT11搭載 デジタル出力
    • 660円 (記事執筆当時)
    • 価格がお手頃であること,そしてプルアップ抵抗やピンヘッダーなどが実装済みで使いやすいことからこちらを選びました.
  • KSY USB電源アダプター 5V/3V 1.5m microUSBコネクター
    • 1870円 (記事執筆当時)
    • もう少し安いのも探せばあったかも...
  • Apacer microSDHC 32GB CL10 UHS-I for Pi
    • 1320円 (記事執筆当時)
    • 最低32GBあるといいかな〜と思ったので,このショップで一番安いこれにしました.
  • タクトスイッチ・小さいブレッドボード
    • 家に転がってたやつ(いただき物)を使うことにしました.まさか使う日が来るとは.電源ボタンの実装に使います

数日待ったら届きました〜〜!ちなみに,SDカードにOSを書き込まないといけませんので,SDカードスロットがないPC/Macをお持ちの方は別途読み込めるようなやつを用意しておいてください.

今回はSSHで自分のPC/Macからアクセスするので,ディスプレイやキーボード,マウスの接続はしません.必要な方はご用意ください.

ラズパイのセットアップ

セットアップは以下のような流れです.

  1. Raspberry Pi OS をSDカードに書き込む.
  2. WiFiやSSHの設定を書き込む
  3. ラズパイを起動させる
  4. SSH接続をする
  5. VNC接続をする (optional)

以上の流れは,こちらの記事に詳しく書きましたので,必要な方はぜひご覧ください!

以下,ラズパイにSSH接続済ということで進んでいきます.

プロジェクトの下準備

さて,いよいよ始めていきましょう.今回は,ラズパイはもちろん,他にも色々なサービスを使っていくので,その準備を一旦まとめてみました.トークンなども発行したりしますので,必要に応じてメモを取りながら進めましょう.

  1. ラズパイにセンサを取り付ける
  2. ラズパイにプロジェクト用のディレクトリを作って,仮想環境を用意して必要なライブラリをインストール
  3. Google Apps Scriptのファイルと,Google Spreadsheetのファイルを用意.また,Google Cloudでプロジェクトを作成し,サービスアカウントを作成し,Sheet APIを有効にする.(PythonからSpreadsheetを操作するのに必要です)
  4. LINE DevelopperからLINE Botを作成し,Messaging APIを有効にする.

1.ラズパイにセンサを接続する

Raspberry Pi Zero の ピン配置は以下のようになっています.

ピン配置
以下のように温度センサ,およびタクトスイッチを接続しましょう.

配線図

実際に配線した写真

2.ラズパイに,プロジェクトのディレクトリ・仮想環境を用意

ラズパイで以下のコマンドをうち,ディレクトリを作成してください.
例として,ディレクトリ名「pj_temperature」として作成してみます.

~%
mkdir ~/pj_temperature

次に,そのディレクトリに移動します.

~%
cd ~/pj_temperature

仮想環境を作成します.例として仮想環境名を「.venv」にします.

~/pj_temperature%
python -m venv .venv

仮想環境をアクティベートします.

~/pj_temperature%
source .venv/bin/activate

今回は以下のライブラリをインストールします!

~/pj_temperature%
pip install dht11
~/pj_temperature%
pip install gspread

以上で,一旦ラズパイの方の設定は終了とします.

3.Google Apps Script と Spreadsheet を用意・Sheet APIを有効にする.

参考になるサイト: サービスアカウントを利用したGoogleスプレッドシートの連携設定

まず,Googleのアカウント(Gmail)を持っていない方がいらっしゃいましたら,Googleアカウントを作成してきてください.

それでは,Google Driveにアクセスし,Google Apps Scriptを作成してください.(今回のプロジェクト用に新しくフォルダを作って,その中で作成するのが良いと思います.)

「More > Google Apps Script」を選択

Google Cloud Consoleに移動します.

Consoleの画面上 (例)
プロジェクトを新しく作成してください.好きなプロジェクト名に変えてくださいね.

そして,API and Service に移動します.ENABLE APIS AND SERVICESをクリックし,出てきた画面で「Google Sheet API」てENABLEします.(画像では,すでにEnableされている画面なので「MANAGE」になってしまっています)

Google Sheet API

次に,「Credential」に移動し,「CREATE CREDENTIALS > Service account」を選択します.

Service accountの選択

Service account name (どんな名前でもいいです),Service account ID (勝手に決まります),Service account description (任意) の3点の入力欄を確認して,「DONE」を押して作成します.

先ほど作成したService account IDのアカウントをクリックして詳細設定のページに進みます.

詳細設定へ進む

「KEYS > ADD KEY > Create new key」の順に進み,keyをJSON形式で作成します.(PCにダウンロードされます.)

このGoogle Sheet APIキーは必ず大切に保存してください!

つぎに,先ほど作成したService account ID (Email)を使用するので,こちらもメモしておいてください.

それでは,温度の情報などを保存するためのSpreadsheetを作成します.
Google Driveにアクセスし,Spreadsheetを作成してください.(先ほど作成したGASと同じフォルダだと便利が良いと思います.)

Google Sheetsを選択

先ほど作成したSheet APIのアカウントからアクセスできるように,Spreadsheetの共有設定を行います.
右上の「Share」ボタンから共有設定画面を開き,入力欄に先ほどメモしたService account ID (Email)を入力し,「Done」を押します.

これで,Apps Script, SpreadsheetとSheet APIの設定が完了しました.

4.LINE DevelopperからLINE Botを作成し,Messaging APIを有効にする.

公式ドキュメント「Messaging APIを始めよう」がとてもわかりやすい.

LINE Developperのサイトに移動し,コンソールにログインします.初回ログイン時には開発者アカウントを作成する必要があります.名前とメールアドレスを入力して作成してください.

ホーム画面で,「Create a new provider」をクリックしてプロバイダー(アプリの提供者情報)を作成します.適当な名前を入力して作成してください.

次に,「Create a Messaging API Channel」左のメニューバーからAdminの自分のProviderを選択して,「Create a channel」を押し,「Messaging API」を選択します.

  • Channel type: Messaging API
  • Company or owner's country or region: Japan
  • Channel icon: 任意(実際にchat画面に表示される画像になります)
  • Channel name: (実際にchat画面に表示される名前になります.いい名前をつけましょう)
  • Category/Subcategory: 一番マッチしそうなやつを選ぶ
  • Email address: 自分のemail

を入力して,「Create」で作成してください.

作成したら,Messaging APIのタブに移動します.

一番下までスクロールすると,Channel Access Tokenというものがあるので,「Issure(発行)」します.

このLine Channel Access Tokenは大切に保管してください!

また,グループラインにBotを導入する可能性のある人は,こちらもEnabledにしておきましょう.(Editから編集できます.)

あとで,こちらのBotの設定を編集することがあるので,このタブを開いたままにしておくとスムーズかと思います.

実際に作ってみる

下準備が終わったということで,後半では実際に作った機能やプログラムを紹介していきます.
5. Raspberry Piを使って測った温度をLINEにPUSH通知する
6. Google Apps Scriptを使って,「温度を教えて」というLINEが来たら,最新の温度をReplyする
7. ラズパイの起動時にプログラムを実行するように設定
8. シャットダウン/起動ボタンを作る

5.Raspberry Piを使って測った温度をLINEにPUSH通知する

温度センサ「DHT11」モジュールを使ってみよう

早速ですが,DHT11を使ってみましょう!DHT11の公式レポジトリにサンプルコードがあるので,それを使ってセンサで値を取ってきます.
pj_temperatureディレクトリに,dht11_test.pyというファイルを作ります.

~/pj_temperature%
nano dht11_test.py
dht11_test.py
import RPi.GPIO as GPIO
import dht11

# initialize GPIO
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM)
GPIO.cleanup()

# read data using pin 18
instance = dht11.DHT11(pin = 18)
result = instance.read()

if result.is_valid():
    print("Temperature: %-3.1f C" % result.temperature)
    print("Humidity: %-3.1f %%" % result.humidity)
else:
    print("Error: %d" % result.error_code)

保存して,実行してみます.

~/pj_temperature%
python dht11_test.py

温度と湿度が出てきたら成功です!ただ,たまにうまく値を読み取れないことがある(Error)が出てきます.エラーコードも参考にしながら,何度が試したり,接続を確認したり,ピン番号の確認をしたりしてみてください.

何度か実行した感じ,室温と1~2度くらいの差がありました.なので今回のプロジェクトでは,誤差も考慮できるように設計していきます.

このコード(dht11_test.py)はプロジェクトに使用しないので,削除しても大丈夫です.

温度をスプレッドシートに記録する

それでは,Google Spreadsheetに日時・温度・湿度を記録していきましょう.

この節で必要となるのは,[Google Apps Script と Spreadsheet を用意・Sheet APIを有効にする.]で作成したGoogle Sheet API key (JSON形式)とSpreadsheetのIDです.SpreadsheetのIDは,そのシートのリンクの「https:// docs.google.com/spreadsheets/d/ここにある値/edit?gid=0#gid=0」の部分になります.英数字記号で構成されていると思います.

まずは,Google Sheet API key をラズパイ側に持たせます.

~/pj_temperature%
nano credentials.json

Google Cloudで作成したGoogle Sheet API keyの内容をコピペしてください.

credentials.json
{
  "type": "service_account",
  "project_id": "XXXXXX",
  "private_key_id": "XXXXXXXXX",
  "private_key": "-----BEGIN PRIVATE KEY-----XXXXXXXXXXXXXXXXXXXXXXX-----END PRIVATE KEY-----\n",
  "client_email": "XXXXXX@XXXXXX.iam.gserviceaccount.com",
  "client_id": "XXXXXXXXXXX",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://oauth2.googleapis.com/token",
  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/XXXXXXXXX.iam.gserviceaccount.com",
  "universe_domain": "googleapis.com"
}

また,SpreadsheetのIDを環境変数として保存します.

~/pj_temperature%
export SPREADSHEET_ID="SpreadsheetのIDをここに書く"

.bashrcファイルにも書いておきましょう.

~/pj_temperature%
nano ~/.bashrc

先ほどと同じコマンドをbashrcファイルに追記します.
``bash:bashrc
export SPREADSHEET_ID="SpreadsheetのIDをここに書く"

そしてbashrcの実行もしておきます.
```bash:~/pj_temperature%
source ~/.bashrc

また,温度の記録用のシートをSpreadsheetに作っておきましょう.今回は「record」という名前のsheetを作りました.1行目にはヘッダーを作成しました.

そして,練習用のファイルとしてgsheet_trial.pyとでも名付けて作成してみます.

~/pj_temperature%
nano gsheet_trial.py
gsheet_trial.py
import RPi.GPIO as GPIO
import dht11
import os
import gspread
from datetime import datetime
import requests
import json
import time

# initialize GPIO
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM)
GPIO.cleanup()

# Settings for Google Spreadsheet
SPREADSHEET_ID = os.getenv('SPREADSHEET_ID') # 環境変数から読み込む
CREDENTIALS = gspread.service_account(filename='/home/username/pj_temperature/credentials.json') # 先ほどのcredentials.jsonの絶対パス.usernameやpj_temperatureをご自身のusernameやディレクトリ名に変更してください
SPREADSHEET = CREDENTIALS.open_by_key(SPREADSHEET_ID)
SHEET_RECORD = SPREADSHEET.worksheet('records') # spreadsheetのシート名

# 日時・温度・湿度をSpreadsheetに書き込む関数
def addData(sheet,date_and_time,temperature,humidity):
    values = sheet.get_all_records() # 全てのレコードを取得
    row_to_write = len(values) + 2 # 次に書き込みができる行
    sheet.update(range_name=f'A{row_to_write}:C{row_to_write}', values=[[date_and_time,temperature,humidity]]) # 書き込み
    return

# DHT11で温度・湿度を取得する
instance = dht11.DHT11(pin = 18)
result = instance.read()
temperature = result.temperature # 温度
humidity = result.humidity # 湿度
# 現在の日時
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# 日時・温度・湿度をSpreadsheetに書き込む
addData(now,temperature, humidity)

コードを保存して実行してみましょう.

~/pj_temperature%
python gsheet_trial.py

スプレッドシートを見に行って,ちゃんと指定したシートに日時・温度・湿度が入力されていたら成功です.

温度が異常値になったら,LINEに通知を送る

このプロジェクトのメイン部分,「異常値で通知を送る」を実装します.

LINEのIDの取得と,通知の送信

LINEのPUSH通知を送る時には,送りたい相手のUser IDや送りたいグループチャットのGroup IDを取得する必要があります.この方法については下記のサブ記事に書きました.以下に沿ってIDの取得および,PUSH通知送信テストを行ってください!

異常値の設定をSpreadsheetで管理をする.

最低気温や最高気温などの異常値の設定は,今回はSpreadsheet上で管理をします.
正直,この後で作成するプログラムに直書きしてもいいのですが... 今回は,機器が手元になくても,クラウド上から設定を変更できるようにしたいので,このようにしました.(例えば,実家で母が「やっぱ通知を送る気温は32度に変えてほしいな〜」とかの要望があったら,私が実家に帰ってラズパイを触らずともインターネットさえあれば変更ができる)

Spreadsheetの内容

各セルの解説:
A2: 最小気温 (この温度以下になったら通知を送る)
B2: 最大気温 (この温度以上になったら通知を送る)
C2: Group ID (LINEの通知を送る先のID.「LINEのIDの取得と,通知の送信」で取得したID)
D2: 測定間隔(分) (この例では10分ごとに温度測定することを示している.)
E2: 測定誤差修正用数値 (今回使用する温度センサは誤差が発生する場合もある.例えば,実際の室温より1度高く出力されがちの場合は,ここに-1を入れることで調整ができるようにする)

異常値検出のためのコード

測定間隔ごとにDHT11モジュールで温度を測定し,異常がある場合にLINEで通知を送ります.
また,異常の有無に関わらず,Spreadsheetに測定値を出力するようにします.
以下のようなプログラムを作成します.

main.py
import RPi.GPIO as GPIO
import dht11
import os
from datetime import datetime
import requests
import json
import time
import gspread

# スクリプトが動作するディレクトリに移動.これは後々の自動起動に関連する部分です.
# このPythonコードの存在するディレクトリを記述してください.
os.chdir('/home/sayaka/shiro_temperature')

# Initialize
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM)
GPIO.cleanup()

# Spreadsheetの設定
SPREADSHEET_ID = os.getenv('SPREADSHEET_ID')
CREDENTIALS = gspread.service_account(filename='/home/sayaka/shiro_temperature/credentials.json')
SPREADSHEET = CREDENTIALS.open_by_key(SPREADSHEET_ID)
SHEET_RECORD = SPREADSHEET.worksheet('records')
SHEET_SETTING = SPREADSHEET.worksheet('settings')

# LINEの設定
LINE_ACCESS_TOKEN = os.getenv('LINE_ACCESS_TOKEN')
LINE_BOT_URL = "https://api.line.me/v2/bot/message/push"

# 詳細設定をSpreadsheetから取得する (プログラムの開始時に実行)
def getSettings():
    sheet = SHEET_SETTING
    # 最低温度
    min_t = float(sheet.acell('A2').value)
    # 最大温度
    max_t = float(sheet.acell('B2').value)
    # 送信先のLINEのgroop ID
    notify_id = sheet.acell('C2').value
    # 測定間隔
    time_interval = int(sheet.acell('D2').value)
    # 温度センサの誤差調整
    temp_diff = float(sheet.acell('E2').value)
    return min_t, max_t,notify_id,time_interval,temp_diff

# Spreadsheetに測定情報を記録する関数
def addData(sheet,date_and_time,temperature,humidity):
    # まずは現在のシートの値を読み込む
    values = sheet.get_all_records()
    # 日時・温度・湿度を書き込む
    # 行がlen(values)+2の場所とすることで,データを一番下に追加する
    sheet.update(range_name=f'A{len(values)+2}:D{len(values)+2}', values=[[date_and_time,temperature,humidity]])
    return

# 通知を実際に送る関数
def notify(notify_id,date_and_time=None,temperature=None,humidity=None, mode='help', free_message=None):
    message=""
    if mode=="free": # free_messageがあるときは,そのメッセージをそのまま送る
        message=free_message
    elif (temperature is None): # temperatureがNoneの時は測定できていないことを通知
        message="[う〜ん]うまく温度が測れませんでした.数分後にもう一度はかります."
    elif mode=='safe': # safe(復活)モードの時は,計り直したらうまくいったことを通知
        message = f"[復活!]次はうまく行きました.{date_and_time}{temperature}度(湿度{humidity})です."
    else: # helpモードの時は,異常温度なのでそれを通知する
        message = f"[助けて〜]{date_and_time}{temperature}度(湿度{humidity})になりました"
    # LINE Messaging APIの設定
    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {LINE_ACCESS_TOKEN}"
	}
    data = {
        "to": notify_id,
        "messages": [{"type":"text", "text":message}]
	}
    # API requestを送信
    try:
        response = requests.post(LINE_BOT_URL, headers=headers, data = json.dumps(data))
        if response.status_code == 200:
            return True
        print(f"Error: {response}")
        return False
    except e:
        print(e)
        return False

# メイン関数
def main():
    # 各種設定値を読み込む
    min_t,max_t,notify_id,time_interval,temp_diff = getSettings()
    # 温度センサ
    instance = dht11.DHT11(pin=18)
    # 温度測定エラーが連続で起こった回数
    count_error=0
    # 再報告を連続で行った回数
    count_to_re_report=0
    # 起動時/再起動時に送るメッセージ
    start_message = f"再起動に成功しました.最低{min_t}度,最高{max_t}度で監視します."
    # start_messageを送信
    notify(notify_id, mode="free", free_message=start_message)

    # ずっと温度を測定する
    while(True):
        try:
            # 温度を測定
            result = instance.read()
            # 現時刻を取得
            now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
            
            if result.is_valid(): # 温度がちゃんと取れていたら
                # 誤差補正用のtemp_diffを加算
                t = result.temperature + temp_diff
                # 湿度 (今回は補正しない)
                h = result.humidity
                # もし連続で10回以上エラーが起きていた後,初めてうまく温度が取れたのなら
                if count_error>10: 
                    # 復活したことを通知
                    notify(notify_id,str(now),t,h,'safe')
                # 今回うまく温度が取れていると言うことは,連続エラー回数は0に戻すべき
                count_error=0
                # 異常温度値の場合は通知を送る
                # (ただし,連続で異常値の場合は3回に一回だけ通知を送信する)
                if (min_t>t or max_t<t) and count_to_re_report%3==0:
                    if notify(notify_id,str(now),t,h):
                        # 通知送信が成功.連続異常通知数であるcount_to_re_reportをインクリメントする
                        count_to_re_report+=1
                        addData(SHEET_RECORD,str(now),t,h)
                    else:
                        # 何らかの原因で通知送信が失敗.count_to_re_reportを0に初期化(こうすることで,次の異常検知時に通知送信をもう一度行える.)
                        count_to_re_report=0
                        addData(SHEET_RECORD,str(now),t,h)
                else: # 異常でない場合
                    count_to_re_report=0
                    addData(SHEET_RECORD,str(now),t,h)
                # うまく温度は取れたので,次はtime_interval分後
                time.sleep(60*time_interval) 
            # 5連続で温度の測定に失敗したら,LINEにその旨を通知する.さらに失敗が続くなら10回に一度温度測定失敗通知をする.
            elif count_error%10==5:
                count_error+=1 # 
                notify(notify_id,str(now))
                addData(SHEET_RECORD,str(now),"Error", "Error")
                time.sleep(60*3)
            # 通知はしないが,温度の測定に失敗だった場合
            else:
                count_error+=1
                time.sleep(60) # errorが起きた時は1分後にもう一度測る
        except Exception as e: # そのほかのエラー
            if report_error==1:
                print("error: (not notified)")
            elif notify(notify_id,str(now)):
                report_error=1
            print(f'{datetime.now().strftime("%Y-%m-%d %H:%M:%S")}:{e}')
            time.sleep(60*3) # うまくいかなかったときは1分待機

if __name__=="__main__":
    try:
        main()
    finally:
        GPIO.cleanup()
					

保存して,実行してみます.

~/pj_temperature%
python main.py

すると,「温度をスプレッドシートに記録する」で登録したスプレッドシート上で温度が記録されているのが確認できると思います.

スプレッドシートに記録された温度 (本当は夏に撮る予定だったけど秋になっていた)

6.Google Apps Scriptを使って,「温度を教えて」というLINEが来たら,最新の温度をReplyする

自動通知はできましたが,「温度を教えて」とLINEでメッセージを送ったら最新の温度を返信してもらう機能もつけてみます.こちらは,Google Apps ScriptとLINE Messaging APIで作成します.
ここで,最新の温度というのは,Spreadsheetに記録されたもののうち一番新しい記録のこととします.LINEのIDの取得と,通知の送信で,一通りGASからLINE Messaging APIを使用することはしていると思います.なので,ここではプログラムの内容についてのみ書き記しておきます.

先に,スクリプトプロパティにLINEのAccess tokenと,温度の記録がされているspreadsheetのIDを登録しておいてください.

Code.gs
// スクリプトプロパティからLINEのAccess tokenを読み込む
const LINE_TOKEN = PropertiesService.getScriptProperties().getProperty("LINE_TOKEN");
// スクリプトプロパティからスプレッドシートのIDを読み込む
const RECORD_SHEET_ID = PropertiesService.getScriptProperties().getProperty("RECORD_SHEET_ID");
// スプレッドシートの中で,温度の記録をしているシートの名前
const RECORD_SHEET_NAME = "records";

// LINE Messaging APIのエンドポイント
const LINE_URL_REPLY = 'https://api.line.me/v2/bot/message/reply';
const LINE_URL_PUSH = 'https://api.line.me/v2/bot/message/push';

// LINEのメッセージを受け取った時に実行される関数
function doPost(e){
  const json = JSON.parse(e.postData.contents);
  const reply_token = json.events[0].replyToken;
  const messageText = json.events[0].message.text;

  if (typeof reply_token === 'underfined') {
    return;
  }

  // もし,メッセージに「温度」という単語が含まれていたら,最新の温度を返す.
  if (messageText.includes('温度')) {
    // 最新の温度情報を取得する
    const temperatureResult = getLatestRecord();
    // 返信内容
    const message = `${temperatureResult[0]}の時点で${temperatureResult[1]}度(湿度${temperatureResult[2]})です!`
    // メッセージを送信
    sendMessage(reply_token,message)
  }
  return;
}

// メッセージを送信するための関数
function sendMessage(reply_token,message){
  const option = {
    'headers': {
      'Content-Type': 'application/json; charset=UTF-8',
      'Authorization': 'Bearer ' + LINE_TOKEN,
    },
    'method': 'post',
    'payload': JSON.stringify({
      'replyToken': reply_token,
      'messages': [{
        'type': 'text',
        'text': message,
      }],
    }),
  }

  UrlFetchApp.fetch(LINE_URL_REPLY,option);
  return
}

// 一番最新の記録をスプレッドシートから取得する関数
function getLatestRecord(){
  const spreadsheet = SpreadsheetApp.openById(RECORD_SHEET_ID);
  const sheet_record = spreadsheet.getSheetByName(RECORD_SHEET_NAME);
  const last_row = sheet_record.getLastRow();
  if (last_row < 2){
    return null
  }
  const last_record = sheet_record.getRange(last_row,1,1,4).getValues()[0];
  return last_record
}

このままだと,スプレッドシートの行数は日に日にどんどん増えていくので,毎日一回消去することにしてみます.ついでに,毎朝,電源が入っているか確認できるようにメッセージを送ることを考えてみます.

Code.gs
// 夜中に一旦全ての記録を削除するための関数
function clearRecords(){
  const spreadsheet = SpreadsheetApp.openById(RECORD_SHEET_ID);
  const sheet_record = spreadsheet.getSheetByName(RECORD_SHEET_NAME);
  const num_records = sheet_record.getLastRow() -1;
  if (num_records>0){
    sheet_record.getRange(2,1,num_records,4).clearContent();
  }
}
// 朝に,記録が1行以上あるかを確認することで機器の不具合を検出・報告する
function isWokeUp(){
  const spreadsheet = SpreadsheetApp.openById(RECORD_SHEET_ID);
  const chat_id = spreadsheet.getSheetByName('settings').getRange('C2').getValue();
  const sendSuccess = spreadsheet.getSheetByName('settings').getRange('F2').getValue();
  const today = spreadsheet.getSheetByName("back_data").getRange('A2').getValue();
  const last_record = getLatestRecord();
  if (last_record == null || new Date(today)>new Date(last_record[0])){
    // 当日の記録がないのなら,電源が入っているかを確認するようにメッセージを送ります
    sendPushMessage(chat_id, "電源が入っているか確認してください.");
  }else if (sendSuccess){
    // ちゃんと記録されているようならば,最新の記録の日時と温度を送ります.
    sendPushMessage(chat_id, `${last_record[0]}時点で${last_record[1]}度です.`);
  }
}
# Push通知を送るための関数
function sendPushMessage(chat_id, message){
  const option = {
    'headers': {
      'Content-Type': 'application/json; charset=UTF-8',
      'Authorization': 'Bearer ' + LINE_TOKEN,
    },
    'method': 'post',
    'payload': JSON.stringify({
      'to': chat_id,
      'messages': [{
        'type': 'text',
        'text': message,
      }],
    }),
  }

  UrlFetchApp.fetch(LINE_URL_PUSH,option);
  return
}

これらの関数を,GASにある時間トリガーの機能を使って毎日定刻に実行するように設定しておきます.
実行時間は,clearRecordsは日付を超えた頃(1AMとか?)にして,isWokeUpは朝(7AM)とかに設定しました.

clearRecordsのトリガー設定 (一部)

isWokeUpのトリガー設定 (一部)

最後に,これをデプロイする必要があります.既にLINEのIDの取得と,通知の送信でLINE Messaging APIにこのGASのデプロイURLを登録している場合は,新規デプロイではなく「デプロイの管理」から更新を行います.

デプロイの管理へ
最新のバージョンを選んでデプロイをします.

新しいバージョンへの更新

7.ラズパイの起動時にプログラムを実行するように設定

参考:Raspberry Pi4起動時に指定したプログラムを実行させる

ここまでで,ほとんど意図通りに動くようになりました.しかし,このままではラズパイを立ち上げた時に手動でpython3 main.pyをしないといけません.
最後に,ラズパイを起動させた時および再起動した時に自動でmain.pyを動かすように設定しておきます.

自動実行ファイルの作成

まずは,自動実行を行うshellファイルを作成しておきましょう.main.pyと同じディレクトリで作成します.

~/pj_temperature%
nano auto_execute.sh

shellファイルの中身は以下のようになっています.ここで,私は仮想環境を利用しているのでそれをアクティベートするためのコードが1行目に入っています.そのため,仮想環境を利用していない人はこの行は必要ないです.

auto_execute.sh

Serviceファイルを作成する

次に,ラズパイ上で,以下のディレクトリに移動します.

~%
cd /etc/systemd/system/

そして,新しいserviceファイルを作成します.(この時,sudo権限が必要です.)

/etc/systemd/system%
sudo nano auto_execute.service

そして,以下のように記述します.環境変数であるSPREADSHEET_IDとLINE_ACCESS_TOKENをここでもう一度定義しました.(もっと賢い方法はあると思う)
ExecStartには,先ほど作成したshellファイルへの絶対パスを,WorkingDirectoryにはmain.pyのあるディレクトリを指定します.Restart=alwaysとすることで,何らかの原因でプログラムが終了したときも再度実行してくれます(おそらく)

suto_execute.service
[Unit]
Description=Temperature_File
[Service]
ExecStart=/home/sayaka/pj_temperature/auto_execute.sh
Environment="SPREADSHEET_ID=XXXXXXXXXXXXXX"
Environment="LINE_ACCESS_TOKEN=XXXXXXXXXXXXXX"
Type=simple
User=sayaka
Group=sayaka
WorkingDirectory=/home/sayaka/pj_temperature
Restart=always
RestartSec=30
[Install]
WantedBy=multi-user.target

それでは,このファイルで実際に実行までできるかを確認しておきましょう.
ファイルをスタートさせます.

/etc/systemd/system%
sudo systemctl start auto_execute.service

ステータスを確認します.

/etc/systemd/system%
sudo systemctl status auto_execute.service

「Active (running)」という文字が出ていたら,成功です.
うまくいかない場合は,auto_execute.shのみの実行をしてみるなど,原因を探す必要があります...
止める場合は以下のコマンドです.

/etc/systemd/system%
sudo systemctl stop auto_execute.service

実際に起動時に自動実行されるように設定をする

最後に起動/再起動時に自動実行するためには,以下のような設定が必要になります.

/etc/systemd/system%
sudo systemctl enable auto_execute.service

また,起動時にすぐに実行するのではなく,ネットワーク接続や機器の状態が安定するまで時間を置いてから実行する方が何かと良いです.そこで,timerファイルも作成してみます.

/etc/systemd/system%
sudo nano auto_execute.timer
```systemd:auto_execute.timer
中身は以下のようにします.onBootSecで待ち時間を調整できます.今回は2分の場合で作成してみました.

[Unit]
Description=auto_execute.service timer
[Timer]
onBootSec=2min
Unit=auto_execute.service
[Install]
WantedBy=multi-user.target

こちらも,起動時に動作するように以下のコマンドを実行します.
```bash:/etc/systemd/system%
sudo systemctl enable test.timer

8. シャットダウン/起動ボタンを作る

参考: RaspberryPiにshutdownボタンを付けよう

最後に,ラズパイにシャットダウン兼起動のための物理ボタンを作成します.
1. ラズパイにセンサを接続する で作成したスイッチを使います.実は,この配線,ラズパイ側の仕様で既に起動ボタンとしての機能は果たせています.
ということで,シャットダウン部分だけ設定を行なっていきます.
まずはconfigファイルを開きます.

~%
sudo nano /boot/firmware/config.txt

config.txtファイルの一番下に,以下の記述を追加します.ここで,debounceには長押しの秒数を設定することができます.下の例のように3000にすれば,3秒の長押しをすることでシャットダウンになる,ということです.

config.txt
dtoverlay=gpio-shutdown,debounce=3000

この設定は再起動時から適用されるので,一旦コマンドからシャットダウンをします.

~%
poweroff

タクトスイッチを押すと,電源を入れることができます.電源を入れて数分後に7.ラズパイの起動時にプログラムを実行するように設定で設定したように自動でプログラムが動いていることを確認してみましょう.
タクトスイッチを長押しすると,シャットダウンができることも確認してみてください.
(main.pyがずっと動いているからなのか,私の押し方が悪いのか,うまくシャットダウンできる時とできない時があるんですよね... もし解決方法ご存知の方がいれば教えてください...)

おわりに

今回は,Raspberry Pi Zeroを初めて自分で購入し,実家の需要に応える形で温度モニタリングシステムを作成しました.記事を書くのはのんびりしたせいで3ヶ月くらいかかりましたが,3日くらいの短納期で作成したシステム(しかも夜にしかなかなか稼働できない)なので,至らぬ点・抜け穴は多々あると思います.

最終的には,適当な紙の箱を工作して部品を全部格納して,こんな感じになりました.

もう秋になってしまい,猛暑はなくなりましたが,現役でまだまだ使ってもらっています.次は寒くなりますしね.

うちの老犬が元気に冬を越して,また来年も猛暑検出に使ってもらえたらいいな,と祈るばかりです.

サブ記事

参考サイト

Raspberry Pi Shop by KSY
Google Cloud
DHT11_Python
Google Spreadsheet用のライブラリ
サービスアカウントを利用したGoogleスプレッドシートの連携設定
LINE Developper
Messaging APIを始めよう
Raspberry Pi4起動時に指定したプログラムを実行させる
RaspberryPiにshutdownボタンを付けよう

Discussion