PR

GASを中継サーバーにLINEからPico Wを遠隔操作する方法|Webhookとポーリングで双方向通信【自作スマートリモコン 第5回】

ハードウェア制御 / 電子工作
スポンサーリンク

はじめに:IoTスマートリモコンの総仕上げ!ラズパイPico WとLINEを連携させよう

自作スマートリモコン 第5回

スポンサーリンク

はじめに:スマホのLINEからエアコンを遠隔操作する

こんにちは、ニンジンです🥕

シリーズ最終回のこの記事では、これまで作ってきた「温湿度を測ってLINEに通知するPico W」を、スマホのLINEから『冷房ON』と送るとエアコンが動くスマートリモコンに仕上げます。カギになるのは、自宅ルーターのファイアウォールを越えて外から命令を届けるしくみです。無料のGoogle Apps Script(GAS)を中継サーバーにして、Webhookとポーリングで疑似的な双方向通信を実現します。

スマホのLINEからGASを経由してPico Wへ命令が届き、赤外線でエアコンを操作する完成イメージ
▲ 完成する自作スマートリモコンの全体像

前提として、第1回〜第4回までが終わっている必要があります。特に第4回で取得した「エアコンの赤外線データ(数字のリスト)」を手元に用意してください。

この記事で分かること

  • Pico Wを外から操作できない理由(ルーターのファイアウォール)とGAS中継の考え方
  • Google Apps Scriptの作成・デプロイ手順(ウェブアプリ/アクセス=全員)
  • doPost(LINE受信)とdoGet(Pico Wからのポーリング)のGASコード
  • LINE DevelopersのWebhook URL設定と応答設定
  • スタンドアロン運用でフリーズを防ぐため、本番main.pyからprint文を消す理由

スポンサーリンク

Pico Wを外から操作できない理由と、GASを使った中継のしくみ

現状、Pico WからLINEへ「暑いよ」と通知を送ることはできています。しかし逆に、スマホのLINEから「冷房をつけて」とPico Wへ直接命令を送ることは簡単にはできません。自宅のWi-Fiルーターがファイアウォールで、外部からの見知らぬアクセスをすべて遮断するためです。

VPNやWebサーバーを立てる方法もありますが手間がかかります。そこで、無料で使えるGoogle Apps Script(GAS)を「インターネット上の私書箱(中継サーバー)」として使い、疑似的な双方向通信を実現します。

LINEからのメッセージをGASが受け取って保存し、Pico Wが定期的に取りに来る中継のしくみ図
▲ GASを私書箱にした中継のしくみ

命令が届くまでの流れ

  1. スマホからLINEに「冷房ON」と送る
  2. LINEのサーバーが、私書箱のGASへ「冷房ONの命令が来た」とメモを残す(Webhook)
  3. 家の中のPico Wが10秒ごとにGASを見に行き(ポーリング)、命令があれば赤外線を発射する

このバケツリレー方式なら、ルーターの設定を一切変えずに、安全に遠隔操作ができます。


スポンサーリンク

中継サーバーの構築:Google Apps Scriptの作成とデプロイ

1. GASプロジェクトを作る

Googleドライブのトップ画面

Googleドライブ にアクセスします。

Googleアカウントのログイン画面

自分のGoogleアカウントでログインします。

Googleドライブの新規作成ボタン

画面左上の「+新規」をクリックします。

新規メニューのその他からGoogle Apps Scriptを選ぶ

「その他」>「Google Apps Script」をクリックします。

Apps Scriptの新規スクリプト作成

「スクリプトを作成」をクリックします。

プロジェクト名の変更

左上の「無題のプロジェクト」をクリックします。

プロジェクト名をPicoW_IR_Chaserに変更

「PicoW_IR_Chaser」など分かりやすい名前に変更します。

エディタに貼り付けるコード

最初から書かれているコードをすべて消し、下のコードを貼り付けて保存します。

// スクリプトプロパティ(GASの簡易データベース)を使って状態を管理します
const PROPERTY_KEY = 'AIR_CON_SIGNAL';

// 1. LINEからメッセージを受け取る関数 (Webhook)
function doPost(e) {
  try {
    const jsonString = e.postData.contents;
    const data = JSON.parse(jsonString);
    
    // LINEからのイベントを取得
    const event = data.events[0];
    if (event && event.type === 'message' && event.message.type === 'text') {
      const userMessage = event.message.text;
      const properties = PropertiesService.getScriptProperties();
      
      // 送られてきたメッセージの内容によって、保存する命令(ステータス)を変える
      if (userMessage.includes("冷房")) {
        properties.setProperty(PROPERTY_KEY, "COOL_ON");
      } else if (userMessage.includes("停止")) {
        properties.setProperty(PROPERTY_KEY, "COOL_OFF");
      } else if (userMessage.includes("現在の温度は")) {
        properties.setProperty(PROPERTY_KEY, "GET_TEMP");
      }
    }
    
    return ContentService.createTextOutput(JSON.stringify({status: 'post_success'}))
                         .setMimeType(ContentService.MimeType.JSON);
  } catch(error) {
    return ContentService.createTextOutput(JSON.stringify({status: 'error', message: error.toString()}))
                         .setMimeType(ContentService.MimeType.JSON);
  }
}

// 2. Pico Wから「命令ある?」と確認しにくる関数
function doGet(e) {
  const properties = PropertiesService.getScriptProperties();
  const currentSignal = properties.getProperty(PROPERTY_KEY) || "NONE";
  
  // Pico Wに現在のステータスを返す
  const responseData = { signal: currentSignal };
  
  // 何かしらの命令(NONE以外)があったら、確認されたのでリセット(消去)する
  if (currentSignal !== "NONE") {
    properties.setProperty(PROPERTY_KEY, "NONE");
  }
  
  return ContentService.createTextOutput(JSON.stringify(responseData))
                       .setMimeType(ContentService.MimeType.JSON);
}

doPost がLINEからのメッセージを受け取り、内容(冷房・停止・現在の温度は)に応じて命令をスクリプトプロパティに保存します。doGet はPico Wが取りに来たときに現在の命令を返し、返したらリセットします。

2. GASをデプロイ(ウェブアプリとして公開)

GASのデプロイボタン

画面右上の「デプロイ」>「新しいデプロイ」をクリックします。

デプロイの種類でウェブアプリを選択

種類の選択(歯車アイコン)で「ウェブアプリ」を選びます。

ウェブアプリのアクセス設定

「次のユーザーとして実行:自分」「アクセスできるユーザー:全員」に設定して「デプロイ」を押します。「全員」でないとPico WやLINEがアクセスできません(「Googleアカウントを持つ全員」ではない点に注意)。

初回は「アクセスの承認」を求められます。「アクセスを承認」→自分のアカウントを選択→「詳細」リンク→「PicoW_IR_Chaser(安全ではないページ)に移動」→「許可」の順で進めます。
デプロイ完了で表示されるウェブアプリのURL

デプロイ完了後に表示される「ウェブアプリのURL」をコピーしてメモします。この後、Pico WとLINEの両方に設定します。


スポンサーリンク

LINE BotとGASを紐付ける:Webhook URLと応答設定

3. LINE DevelopersでWebhook URLを設定

LINE Developersコンソール

LINE Developers のコンソールにログインします。

Messaging APIチャネルを開く

第2回で作ったMessaging APIチャネル「温湿度自動アラート」を開きます。

Messaging API設定タブ

「Messaging API設定」タブを開きます。

Webhook URLにGASのURLを設定してオンにする

「Webhook設定」の「Webhook URL」に、先ほどコピーしたGASのウェブアプリURLを貼り付けて保存し、「Webhookの利用」をオンにします。

4. LINE Official Account Managerで応答設定

LINE Official Account Managerのアカウント選択

LINE Official Account Manager にログインし、「温湿度自動アラート」をクリックします。

応答設定メニュー

画面を下にスクロールして「応答設定」をクリックします。

応答設定でチャットオフ・Webhookオン・応答メッセージオフ

「チャット:オフ」「Webhook:オン」「応答メッセージ:オフ」に設定します(あいさつメッセージは任意)。これで中継サーバーの準備は完了です。


スポンサーリンク

Pico Wに最終プログラム(main.py)を書き込む

Pico Wに書き込む最終コードの設定箇所

いよいよ大詰めです。下のコードを貼り付け、上部の設定(Wi-Fi情報・LINEトークン・USER_ID・取得したGASのURL)を自分の値に書き換えます。さらに第4回で取得したエアコンの赤外線データを COOL_ON_DATA / COOL_OFF_DATA に貼り付けます。

#main.py

import machine
import dht
import network
import time
import urequests
import json
import gc

# 起動時のパーツ立ち上がりを待つ(超重要!)
time.sleep(3)

# --- 1. ご自身の環境に書き換えてください ---
WIFI_SSID = "あなたのWi-Fi_SSID"
WIFI_PASSWORD = "あなたのWi-Fi_パスワード"
LINE_TOKEN = "あなたのLINE_チャンネルアクセストークン"
USER_ID = "あなたのLINE_ユーザーID"

# 🔴 先ほど取得したGASのウェブアプリURL
GAS_URL = "https://script.google.com/macros/s/XXXXX/exec"

# --- 2. 見守りとピンの設定 ---
ALERT_TEMP = 28          # 警告温度
CHECK_INTERVAL = 10      # チェック間隔(秒)
TX_PIN = 17              # 赤外線LEDピン

# --- 3. エアコン信号データ(※前回取得した数字のリストに書き換えてください) ---
COOL_ON_DATA = [4307, 4535, 480, 1667, ...] 
COOL_OFF_DATA = [4368, 4507, 476, 1605, ...] 

sensor = dht.DHT11(machine.Pin(15))
wlan = network.WLAN(network.STA_IF)

def connect_wifi():
    wlan.active(True)
    if not wlan.isconnected():
        wlan.connect(WIFI_SSID, WIFI_PASSWORD)
        timeout = 15
        while not wlan.isconnected() and timeout > 0:
            time.sleep(1)
            timeout -= 1
        if not wlan.isconnected():
            machine.reset()

def send_ir(data):
    pwm = machine.PWM(machine.Pin(TX_PIN))
    pwm.freq(38000)
    def pwm_on(): pwm.duty_u16(21845)
    def pwm_off(): pwm.duty_u16(0)
    is_on = True
    for us in data:
        if is_on: pwm_on()
        else: pwm_off()
        time.sleep_us(us)
        is_on = not is_on
    pwm_off()
    pwm.deinit()

def send_line(message):
    response = None
    try:
        connect_wifi()
        url = "https://api.line.me/v2/bot/message/push"
        headers = {"Content-Type": "application/json; charset=UTF-8", "Authorization": f"Bearer {LINE_TOKEN}"}
        payload = {"to": USER_ID, "messages": [{"type": "text", "text": message}]}
        response = urequests.post(url, data=json.dumps(payload).encode('utf-8'), headers=headers)
    except Exception:
        pass
    finally:
        if response:
            try: response.close()
            except: pass

def check_gas_command():
    response = None
    try:
        connect_wifi()
        response = urequests.get(GAS_URL)
        if response.status_code in [301, 302, 303, 307, 308]:
            redirect_url = response.headers.get('location') if hasattr(response, 'headers') else None
            response.close()
            response = None
            if redirect_url: response = urequests.get(redirect_url)
            else: return "NONE"
        
        text_data = response.text
        if text_data.startswith('<'): return "NONE"
        return json.loads(text_data).get("signal", "NONE")
    except Exception:
        return "NONE"
    finally:
        if response:
            try: response.close()
            except: pass

# --- メイン処理 ---
send_line("遠隔エアコン操作システムが起動しました!監視と命令待ちを開始します。")
alert_sent = False
error_count = 0

while True:
    try:
        sensor.measure()
        temp = sensor.temperature()
        humidity = sensor.humidity()
        
        if temp >= ALERT_TEMP:
            if not alert_sent:
                send_line(f"【警告】室温が{temp}°Cになりました!エアコンを確認してください。(湿度: {humidity}%)")
                alert_sent = True
        else:
            if alert_sent:
                send_line(f"室温が{temp}°Cに下がりました。安全圏内です。")
                alert_sent = False
                
        cmd = check_gas_command()
        if cmd == "COOL_ON":
            send_ir(COOL_ON_DATA)
            send_line(f"エアコンに冷房ON信号を送信しました。(現在の室温: {temp}°C)")
        elif cmd == "COOL_OFF":
            send_ir(COOL_OFF_DATA)
            send_line(f"冷房OFFの信号を発信しました。(現在の室温: {temp}°C)")
        elif cmd == "GET_TEMP":
            send_line(f"現在のお部屋の状況です。\n温度: {temp}°C\n湿度: {humidity}%")
            
        error_count = 0
            
    except Exception:
        error_count += 1
        time.sleep(5)
        if error_count >= 5: machine.reset() # 連続エラー時はシステム再起動
        
    finally:
        gc.collect() # メモリ解放
        time.sleep(CHECK_INTERVAL)

check_gas_command() がGASにポーリングし、返ってきた命令(COOL_ON / COOL_OFF / GET_TEMP)に応じて send_ir() で赤外線を発射します。script.google.comは302リダイレクトを返すことがあるため、リダイレクト先を追う処理も入れてあります。

なぜ本番のmain.pyには「print文」が1つも無いのか

テスト用コードには print("冷房ONを検知") のような出力がたくさんありましたが、本番の main.py ではあえて全部消しています。これはスタンドアロン運用でのフリーズを防ぐためです。

PC(Thonny)につないでいる間は、print の出力をPCが受け取ってくれます。しかしPCから抜いてコンセント給電にすると、出力の受け取り先がいなくなります。受け取り先が無いまま print し続けると、Pico W内部のバッファに送信待ちのテキストが溜まり続け、やがてメモリを圧迫してシステムごと停止します。

本番では print を無くし、except は pass で流し、gc.collect() でこまめにメモリを解放します。連続エラー時は machine.reset() で自動再起動させることで、長期間ほったらかしでも安定して動きます。

スポンサーリンク

スマホからエアコンを遠隔操作するテスト

まずThonnyの実行ボタンでエラーが出ないか確認します。LINEに起動メッセージが届いたら、スマホから「冷房ON」「停止」「現在の温度は」と送ってみます。

Thonnyでmain.pyを実行した直後の画面

Thonnyで main.py を実行すると、ターミナルに「soft reboot」と表示されます。

起動通知がLINEに届いた画面

スマホに「遠隔エアコン操作システムが起動しました!監視と命令待ちを開始します。」と届きます。

冷房ON命令と完了通知がLINEに届いた画面

「冷房ON」と送ると、10秒以内に「エアコンに冷房ON信号を送信しました。」と返り、エアコンから「ピッ」と音が鳴って冷房が入ります(赤外線LEDをエアコンの受光部へ向けておきます)。

室温超過の警告メッセージ

温湿度センサーに息を吹きかけて温度を上げると、これまでどおり警告メッセージが届きます(第3回の見守り機能はそのまま動いています)。

アラート解除メッセージ

27℃以下に下がると「室温が◯◯℃に下がりました。安全圏内です。」と解除メッセージが届きます。

冷房OFF命令と完了通知

「停止」と送ると、10秒以内に「冷房OFFの信号を発信しました。」と返り、エアコンが止まります。

現在の温湿度の問い合わせ結果

「現在の温度は」と送ると、10秒以内に現在の温度と湿度が返ってきます。


スポンサーリンク

うまく動かないときの定番トラブルと解決策

Q1LINEから「冷房」と送ってもPico Wが反応しない

A原因の大半はWebhook URLの更新忘れです。GASのコードを少しでも変えたら「新しいデプロイ」でURLを再発行し、Pico Wの GAS_URL とLINE DevelopersのWebhook URLの両方を貼り直します。URLを据え置きたい場合は「デプロイの管理」から既存デプロイを編集します。

Q2Thonnyに「HTMLを受信」「JSONエラー」と出る

APico WがGASにアクセスしたとき、JSONではなくGoogleのログイン画面(HTML)が返っています。GASのデプロイ設定で「アクセスできるユーザー」が「全員」になっているか確認してください(「Googleアカウントを持つ全員」ではありません)。

Q3SyntaxError: invalid syntax で止まる

APythonはインデント(行頭の空白)でブロックを判断します。コピペで if / elif の縦位置がずれた、行末の : が消えた、が典型例です。エラー行の周辺の空白と記号を見直してください。


スポンサーリンク

PC不要にする:Pico Wにmain.pyを保存して独立稼働させる

第3回のおさらいです。テストが通ったら、プログラムをPico W本体に main.py という名前で保存します。この名前で保存すると、電源が入った瞬間に自動実行されます。

名前を付けて保存メニュー

Thonnyの「ファイル」>「名前を付けて保存」を選びます。

保存先にRaspberry Pi Picoを選ぶ

保存先に「Raspberry Pi Pico」を選びます。

ファイル名をmain.pyにして保存

ファイル名を main.py にして保存します。

保存できたらUSBを抜き、5V ACアダプター(スマホの充電器で可)でコンセントに挿します。数秒後にWi-Fiへ自動接続し、スマホに起動通知が届けばスタンドアロン化の完了です。PCの電源が切れていても、24時間の見守りとエアコン操作ができるようになります。

📡 参考・第3回 Pico WからLINEへ通知を送るMicroPythonコード|urequestsでプッシュ送信・日本語エラー400対策 main.py保存の詳しい手順と、見守り部分のコード解説はこちら。 →

スポンサーリンク

おわりに:ハードからクラウドまで一気通貫で作った自作IoT

全5回、おつかれさまでした。ただの小さな基板だったRaspberry Pi Pico Wが、電子パーツで物理世界とつながり、MicroPythonで動きを与えられ、Wi-FiとGASを経由してLINEというクラウドサービスと結びつきました。ハードウェア・ソフトウェア・ネットワークを通しで作るのが、IoT開発のいちばん面白いところです。

市販のスマートリモコンを買えば数千円で済みますが、自分でトラブルを潰しながら仕組みごと作ったこのシステムは、つぶしの利く生きたノウハウになります。

前の記事・関連記事

📡 前の記事・第4回 ラズパイPico Wで赤外線リモコンを自作|受信・送信回路と38kHz PWM制御 この記事で使うエアコンの赤外線データは、第4回で取得します。 → 🗺️ 総集編・全体像 ラズパイPico WとLINE Botで作る自作スマートリモコン|全5回ロードマップ 第1回〜第5回の全工程と完成システムの全貌をまとめています。 → 🌡️ 関連・別の作例 Raspberry Pi Pico Wで作る熱中症警戒アラート表示器 同じPico W+DHT11に0.96インチOLEDを足して、暑さ指数を顔アイコンで表示する工作です。 →

スポンサーリンク

よくある質問(FAQ)

QGASのウェブアプリは無料ですか?制限はありますか?

A無料です。ただしGASには1日あたりの実行回数やUrlFetchの上限(クォータ)があります。10秒間隔のポーリングとたまの操作程度なら通常は上限に達しません。反応が不要な夜間はPico W側で間隔を延ばすと余裕が出ます。

Qポーリング間隔(CHECK_INTERVAL)は変えられますか?

Aコード上部のCHECK_INTERVALで変更できます。短くするとLINEを送ってからエアコンが動くまでが速くなりますが、GASの1日あたり実行回数の上限に近づきます。10〜15秒が実用的なバランスです。

Q「アクセスできるユーザー:全員」はセキュリティ的に問題ないですか?

AウェブアプリのURLは推測が困難な長い文字列です。doGetは現在の命令を返すだけなので大きなリスクは小さいですが、気になる場合はdoPostでLINEの署名(X-Line-Signature)を検証する処理を追加してください。

QGASのコードを修正したら必ずやることは何ですか?

A「新しいデプロイ」でURLを再発行し、そのURLをPico WのGAS_URLとLINE DevelopersのWebhook URLの両方に貼り替えます。URLを変えたくない場合は「デプロイの管理」から既存デプロイを編集して上書きします。

QLINEの通知は来るのに、エアコンが反応しません。

ACOOL_ON_DATA / COOL_OFF_DATA が第4回で取得した実データに置き換わっているか、赤外線LEDの配線(TX_PIN=GP17)と向き、給電が足りているか(USB給電より2A以上のACアダプタ)を確認してください。

Q数時間でPico Wがフリーズします。

Aprint文が残っていないか、exceptをpassで流しているか、gc.collect()が入っているかを確認してください。本記事のmain.pyはこれらの対策を最初から入れてあります。



ニンジン🥕アイコン

Follow me on X

本ブログの中の人「ニンジン🥕」はXやってます。新着記事はいち早くポストでお知らせしているので、見逃したくない方は @ninjin_py_vba をフォローしてみてください🥕

@ninjin_py_vba をフォロー

この記事が役に立ったら、シェアしてもらえると嬉しいです🥕

Xでポスト はてブ LINE Pocket

PROJECT LOG — SERIES INDEX
スポンサーリンク

シリーズ記事一覧

電子工作・自動化ツールの製作記録をシリーズごとにまとめています。気になるテーマからお読みください。


GPSロガーのCSVを地図に表示する方法|Pico 2 WでGT-502MGG-NとATGM336Hを車に積んで走行・トンネル比較【Python・folium】
GPSロガーで記録したCSVを、Pythonのfoliumと地理院タイルで地図に表示する方法。GT-502MGG-NとATGM336Hを車に積んで4回走り、再起動の原因やトンネルでの違いも比べました。
今週の注目ガジェット〜10/19週〜|電子工作・自作向け新製品まとめ(Romu/W1700K SDR ほか)
大手が拾わないニッチな新製品を毎週チェック。今週はe-Paper開発キット、超小型USBボード、自作SDRなど5本立て。
GT-502MGG-NとATGM336Hの使い方|Raspberry Pi Pico 2 WとMicroPythonでGPSを受信して2台を比較
GT-502MGG-NとATGM336Hの使い方を、Pico 2 W×MicroPythonで解説。北斗で衛星数が多いATGM336H、信号が強いGT-502MGG-N。単精度floatで緯度経度が約1.4m刻みになる落とし穴も紹介します。
今週の注目ガジェット〜10/12週〜|電子工作・自作向け新製品まとめ(Q8botOne/Sipeed SLogic32U3 ほか)
大手が拾わないニッチな新製品を毎週チェック。今週は四足歩行ロボット、ロジックアナライザー、広告ブロッカーなど5本立て。
MOSFETはPicoの3.3Vで足りる?DCソレノイド駆動でゲート電圧不足を実測【Pico 2 W】
Pico 2 WとMOSFET(IRLU3410)で12VのDCソレノイドを駆動。ゲート3.3Vでは発振し、オン抵抗も約2.4倍に。レベル変換で5V駆動にした比較と、電流が8割止まりだった本当の原因を実測で紹介します。
まとめハブ
ハードウェア制御 / 電子工作
スポンサーリンク

コメント