🥬

ラズパイ × 土壌湿度センサ × LINE通知で植木鉢の水やりアラートを作る

に公開

はじめに

植物の水やりを忘れて枯らしてしまう――そんな経験はありませんか?
本記事では Capacitive Soil Moisture Sensor V1.2(静電容量式土壌湿度センサ)と Raspberry Pi 3B+ を使って、土壌水分を計測し、閾値判定の結果を LINE に通知する仕組みを紹介します。

ポイントは以下です。

  • Raspberry Piでセンサ値を取得(MCP3008を介したADC)
  • 判定結果を Google Apps Script(GAS) の Web アプリへ POST
  • GAS が LINE Messaging API を呼び出して通知
  • ログは週ごとに CSV に保存(Excelでの可視化を想定)

使用デバイス

  • Raspberry Pi 3B+
  • Capacitive Soil Moisture Sensor V1.2(アナログ出力)
  • MCP3008(SPI接続の10bit ADC)
  • ジャンパ線・ブレッドボード

(電源は 3.3V 系で統一することをおすすめします)


システム構成(簡易図)


配線のポイント

MCP3008 ↔ Raspberry Pi (SPI)

MCP3008 Raspberry Pi(GPIO)
VDD, VREF 3.3V
AGND, DGND GND
CLK GPIO11 (SCLK)
DOUT GPIO9 (MISO)
DIN GPIO10 (MOSI)
CS GPIO8 (CE0)

センサ ↔ MCP3008

  • Sensor VCC -> 3.3V(推奨)
  • Sensor GND -> GND
  • Sensor AOUT -> MCP3008 CH0

注意: Raspberry Pi の GPIO は 3.3V 領域です。5V を直接流すと破損する恐れがあります。センサやADCの電源は3.3V系で揃えてください。


センサ値とキャリブレーション

  • 静電容量型センサは安定性が高く腐食しにくいですが、土質や鉢・設置深さで出力が変わります。
  • まず「完全に乾いた土」と「十分に湿った土」で実測して、DRY/WET の基準値を決めてください。

例(あくまで実測例)

  • DRY ≒ 700
  • WET ≒ 300

閾値の例(運用例)

  • THRESH_HIGH = 600 → これ以上で「求水」(乾燥)
  • THRESH_LOW = 400 → これ以下で「充水」(湿潤)
  • その間は「様子見」

Google Apps Script (GAS) 側の実装

GASを Web アプリとしてデプロイし、RPi からの POST を受け取り LINE に通知します。
実運用では スクリプトプロパティ に LINE のチャネルアクセストークンや通知先を入れておくと安全です。

手順(概要)

  1. Google ドライブ → 新規 → スクリプトを作成
  2. 下記のコードを貼り付け
  3. スクリプトのプロパティに LINE_TOKENLINE_TO を設定
  4. デプロイ → 新しいデプロイ → 種類「ウェブアプリ」→ 公開設定を適切に(開発中は自分のみ、必要なら匿名可)
// Code.gs
// ==============================
// LINE Messaging API 用設定
// ==============================

// スクリプトプロパティからアクセストークンを取得
const CHANNEL_ACCESS_TOKEN = PropertiesService.getScriptProperties().getProperty('CHANNEL_ACCESS_TOKEN');

// LINE ユーザーID
const USER_ID = 'XXX'; //LINEアカウントで確認して記載


// ==============================
// doPost 関数 (本番用)
// ==============================

/* GAS Webhook受信用関数
   HTTP POSTリクエスト受信時に実行される
   引数"e"は「イベントオブジェクト」で、送られてきたデータやパラメータが入っている
*/
function doPost(e) {

  /* msgにPOSTリクエストで受信したmessageの内容を格納 
     messageが空の場合は、固定値を代入する 
  */
  var msg = e.parameter.message || "メッセージが空です!";
  var messages = [];

  /* msgが"image"形式の場合 */
  if (msg === "image") {

    /* 画像を送信(例:公開URLの画像を使用)*/
    var imageUrl = e.parameter.imageUrl || "https://i.imgur.com/q4yV02g.jpeg";

    /* messages配列に属性を追加 */
    messages.push({
      type: "image",
      originalContentUrl: imageUrl,
      previewImageUrl: imageUrl
    });

  /* msgが"image"形式でない場合 */
  } else {
    // テキストを送信
    messages.push({
      type: 'text',
      text: msg,
    });
  }

  /* USER_IDとmessagesを紐づけ、LINE APIで送信する配列に格納 */
  var payload = {
    to: USER_ID,
    messages: messages
  };



  /* エラーが発生する可能性がある処理を実行 */
  try {

  /* [UrlFetchApp.fetch]第一引数が送信先URL、第二引数でリクエストのオプション(メソッド、ヘッダー、データなど)を指定する,GASメソッド */
    var response = UrlFetchApp.fetch("https://api.line.me/v2/bot/message/push", { //LINE Messaging API の「プッシュメッセージ」エンドポイント
      method: "post",   //POSTはデータ送信
      contentType: "application/json",    // JSON形式のデータ送付
      headers: {
        "Authorization": "Bearer " + CHANNEL_ACCESS_TOKEN //Authorizationに「Bearer + アクセストークン」を設定し、LINE API に正しい権限を持っているアプリと伝える
      },
      payload: JSON.stringify(payload), //実際に送るデータ(本文)
    });

    /* LINE APIなどに送信した結果の本文をログに残す*/
    Logger.log("Response: " + response.getContentText());

  /* try部分エラー時に実行される,ログにエラーを残す */
  } catch (error) {
    Logger.log("Error: " + error.message);


    return ContentService.createTextOutput("Error occurred: " + error.message);
  }

  return ContentService.createTextOutput("OK");
}

// ==============================
// デバッグ用関数
// ==============================
function testDoPost() {
  // デバッグ用モックイベント
  var e1 = { parameter: { message: "テスト1" } };
  var e2 = { parameter: { message: "テスト2" } };
  Logger.log(doPost(e1).getContent());
  Logger.log(doPost(e2).getContent());
}

function doGet(e) {}

セキュリティ補足

  • Web アプリを「全員(匿名含む)」にした場合は、**POST 送信元の検証(秘密トークン)**を実装してください。
  • GAS側で data.secret を受け取り、事前に共有した値と比較する方法が簡単です。

Raspberry Pi 側の実装(Python)

依存パッケージ

sudo apt update
sudo apt install -y python3-pip
pip3 install spidev requests

SPIを有効化(sudo raspi-config → Interface Options → SPI → Enable)してください。


send_line_sensorvalue.py(例)

import spidev
import time
import requests
import csv
import os
from datetime import datetime

# --- Google Apps Script経由でLINEに送信するためのURL(公開範囲は「全員」)
GAS_URL = "XXXX" # GASでデプロイした際のURLを記載

def init_spi():
    """
    MCP3008用SPIデバイスの初期化
    """
    spi = spidev.SpiDev()
    spi.open(0, 0)  # バス0, CE0
    spi.max_speed_hz = 1350000  # MCP3008対応クロック
    return spi

def read_adc_average(spi, channel, count=10):
    """
    指定チャネルからADC値を複数回取得し、その平均値を返す
    """
    values = []
    for _ in range(count):
        adc = spi.xfer2([1, (8 + channel) << 4, 0])  # MCP3008通信
        value = ((adc[1] & 3) << 8) + adc[2]         # 10bit値に変換
        values.append(value)
        time.sleep(0.05)  # 50ms間隔で取得(ノイズ対策)
    avg_value = int(sum(values) / len(values))
    return avg_value

def send_line_message(message):
    """
    LINE(GAS経由)にメッセージを送信
    """
    try:
        response = requests.post(GAS_URL, params={"message": message})
        if response.status_code == 200:
            print("LINEにメッセージ送信成功!")
        else:
            print(f"エラー発生!HTTPステータスコード: {response.status_code}")
    except Exception as e:
        print(f"通信エラー: {e}")

def write_log(value, log_file="log.csv"):
    """
    センサ値をCSVファイルに追記(タイムスタンプ付き)
    """
    file_exists = os.path.isfile(log_file)
    now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    with open(log_file, "a", newline="") as f:
        writer = csv.writer(f)
        if not file_exists:
            writer.writerow(["timestamp", "sensor_value"])
        writer.writerow([now, value])

def check_flags(value, prev_flag):
    """
    センサ値に応じて求水/充水フラグを判定し、必要ならLINEにメッセージ送信
    ヒステリシスあり(同じメッセージの連続送信防止)
    """
    # 求水フラグ: 550超でON、450未満でOFF
    # 充水フラグ: 350未満でON、450超でOFF
    if prev_flag != "求水" and value > 550:
        send_line_message("のどがかわいた…")
        return "求水"
    elif prev_flag == "求水" and value < 450:
        return None  # 求水フラグ解除

    if prev_flag != "充水" and value < 350:
        send_line_message("おなかいっぱい!")
        return "充水"
    elif prev_flag == "充水" and value > 450:
        return None  # 充水フラグ解除

    return prev_flag  # フラグ変更なし

def main():
    """
    センサ値を監視し、状態に応じてLINE送信&10分ごとにログ保存
    10秒ごとにセンサ値(平均)を取得
    """
    print("センサ値を監視し、状態に応じてLINE送信&10分ごとにログ保存します。Ctrl+Cで終了。")
    spi = init_spi()
    prev_flag = None  # ← ファイルからの復元をやめて、毎回Noneから開始
    last_log_time = time.time()  # 最後にログ保存した時刻

    try:
        while True:
            # センサ値を10回取得し平均値を算出
            avg_value = read_adc_average(spi, 0, count=10)
            print(f"センサ値(平均): {avg_value}")

            # 状態判定&LINE送信
            prev_flag = check_flags(avg_value, prev_flag)

            # 10分ごとにCSVログ保存
            now = time.time()
            if now - last_log_time >= 600:
                write_log(avg_value)
                print("10分ごとにセンサ値をlog.csvに保存しました。")
                last_log_time = now

            time.sleep(10)  # 10秒毎に計測
    except KeyboardInterrupt:
        print("プログラムを終了します。")
    finally:
        spi.close()

if __name__ == "__main__":
    main()

Excelでの可視化(週ごとファイル)

  • 1ファイル = 1週間分の時系列データ(最大168点 = 24×7)
  • ExcelでCSVを開き、time を X 軸、moisture を Y 軸にして折れ線グラフを作成します

セキュリティと運用上の注意

  • GASの公開設定: Webアプリを匿名公開すると誰でもPOST可能になるため、secret トークンを使った認証をGAS側で実装することを推奨します。
  • 電源断対策: モバイルバッテリ駆動で突然電源が落ちると SD カード破損のリスクがあるため、UPS HAT や電源管理ボード(PiJuice、Waveshare UPS HAT、PiSugarなど)を検討してください。
  • 電圧レベル: ADCやセンサは3.3V運用を基本に。5V出力のまま接続しない。

トラブルシューティング(よくあるハマりどころ)

  • 値が不安定 → センサ接触不良、ノイズ、土壌の接触面を確認。複数サンプルの平均化が有効。
  • GASの403/401 → LINEトークン/デプロイ権限を確認。

今後の拡張アイデア

  • ハードウェア信頼性の確保(防水性、防塵性)
    → PCB基板及びコネクタ作成
    → センサ保護ケースの作成
  • 大容量バッテリ、ソーラー充電実装
    →パススルーで充電可能なバッテリ必要
  • パラメータの外部管理化(YAML使う)
  • データをスプレッドシートやクラウド(BigQuery,GoogleDrive等)にためて長期解析
  • 温湿度センサを追加して環境監視を強化
  • LINEのリッチメニューやボタンで「水やり済み」を手動記録
  • 電磁弁+ポンプで自動給水(ハードと安全回路の検討が必須)

まとめ

本記事では、Raspberry Pi 3B+ と Capacitive Soil Moisture Sensor V1.2 を用いて、土壌湿度を週単位でログ取りしつつ、閾値判定で LINE に通知するシステムを紹介しました。シンプルなフローなので、まずは動作確認→キャリブレーション→運用に移すと良いでしょう。
※記事の一部に生成AIを使用しています。

Discussion