Daily Hack
📡

Raspberry Pi 5で外出先からエアコンを操作できるようにしてみた

デジタル
#Raspberry Pi#自動化#IoT#スマートホーム#赤外線

「帰宅前にエアコンを付けておきたい」「消し忘れた照明を外出先からオフにしたい」——そんな悩みを、Raspberry Pi 5 と赤外線 LED で解決できます。

この記事では、既存の家電のリモコン信号を学習して、スマホからいつでもどこでも家電を操作できるリモコンサーバーの作り方を解説します。

IR リモコンサーバー 動作の仕組み

完成イメージ

  • スマホのブラウザから家電を ON/OFF
  • エアコン、照明、テレビなど赤外線リモコン対応の家電すべてに対応
  • 外出先からも操作可能(VPN またはトンネリング経由)
  • タイマー設定で自動操作も可能

必要なもの

| パーツ | 目安価格 | |--------|----------| | Raspberry Pi 5(4GB 以上) | 約10,000円 | | microSD カード(32GB 以上) | 約1,000円 | | USB-C 電源アダプター | 約2,000円 | | 赤外線 LED(940nm) | 約100円 | | 赤外線受信モジュール(VS1838B 等) | 約100円 | | NPN トランジスタ(2SC1815 等) | 約50円 | | 抵抗 100Ω × 1、10kΩ × 1 | 約20円 | | ブレッドボード+ジャンパーワイヤー | 約500円 |

赤外線 LED とトランジスタは電子部品店やネットで購入できます。秋月電子の「赤外線リモコンキット」がまとめて手に入るので便利です。

STEP 1: LIRC をインストールする

LIRC(Linux Infrared Remote Control)は、赤外線信号の送受信を制御するソフトウェアです。

sudo apt update
sudo apt install -y lirc

GPIO の設定

/boot/firmware/config.txt を編集します。

sudo nano /boot/firmware/config.txt

ファイル末尾に以下を追加します。

dtoverlay=gpio-ir,gpio_pin=22
dtoverlay=gpio-ir-tx,gpio_pin=23

| 設定 | GPIO ピン | 用途 | |------|----------|------| | gpio-ir | GPIO 22 (Pin 15) | 赤外線受信 | | gpio-ir-tx | GPIO 23 (Pin 16) | 赤外線送信 |

再起動します。

sudo reboot

STEP 2: 赤外線回路を配線する

受信回路(リモコン信号の学習用)

| VS1838B ピン | 接続先 | |-------------|--------| | OUT(左) | GPIO 22 (Pin 15) | | GND(中) | GND (Pin 6) | | VCC(右) | 3.3V (Pin 1) |

送信回路(家電の操作用)

赤外線 LED は GPIO から直接駆動すると光量不足になるため、トランジスタで増幅します。

GPIO 23 (Pin 16) --- [100Ω 抵抗] --- トランジスタ(ベース)
                                      トランジスタ(エミッタ) --- GND
5V (Pin 2) --- 赤外線LED(アノード) --- 赤外線LED(カソード) --- トランジスタ(コレクタ)

赤外線 LED は肉眼では見えませんが、スマホのカメラを通すと光っているか確認できます。送信テスト時にはカメラで確認しましょう。

STEP 3: LIRC の設定を行う

LIRC 設定ファイルの編集

sudo nano /etc/lirc/lirc_options.conf

以下の2行を変更します。

driver    = default
device    = /dev/lirc0

LIRC の再起動

sudo systemctl restart lircd
PRスポンサーリンク
スポンサーリンク

STEP 4: リモコン信号を学習する

4-1. LIRC サービスを一時停止

sudo systemctl stop lircd

4-2. 信号を記録する

mode2 -d /dev/lirc0

この状態でリモコンを受信モジュールに向けてボタンを押すと、パルスデータが表示されます。データが表示されれば受信回路は正常です。Ctrl+C で終了します。

4-3. irrecord でリモコンを登録する

sudo irrecord -d /dev/lirc0 ~/lircd.conf

対話式のウィザードが起動します。手順に従ってください。

  1. デバイス名を入力(例: aircon, light, tv
  2. 指示に従ってリモコンのボタンをランダムに押す(信号パターンの学習)
  3. 各ボタンの名前を入力してボタンを押す
# ボタン名の例
KEY_POWER       (電源)
KEY_VOLUMEUP    (音量UP)
KEY_VOLUMEDOWN  (音量DOWN)
KEY_CHANNELUP   (チャンネルUP)

エアコンのリモコンは信号が複雑なため、irrecord ではうまく記録できないことがあります。その場合は mode2 の生データを使う方法(後述)を試してください。

4-4. 設定ファイルを配置する

sudo cp ~/lircd.conf /etc/lirc/lircd.conf.d/myremote.lircd.conf
sudo systemctl restart lircd

4-5. 送信テスト

irsend SEND_ONCE myremote KEY_POWER

家電の電源が入れば成功です。

STEP 5: Web API サーバーを作る

スマホから操作するための Web サーバーを構築します。

python3 -m venv ~/ir-remote
source ~/ir-remote/bin/activate
pip install flask
# ir_server.py
from flask import Flask, jsonify, render_template_string
import subprocess

app = Flask(__name__)

# === リモコンコマンド定義 ===
DEVICES = {
    "aircon": {
        "name": "エアコン",
        "remote": "aircon",
        "commands": {
            "power": {"name": "電源", "key": "KEY_POWER"},
        },
    },
    "light": {
        "name": "照明",
        "remote": "light",
        "commands": {
            "on": {"name": "ON", "key": "KEY_POWER"},
            "off": {"name": "OFF", "key": "KEY_POWER2"},
            "bright": {"name": "明るく", "key": "KEY_BRIGHTNESSUP"},
            "dim": {"name": "暗く", "key": "KEY_BRIGHTNESSDOWN"},
        },
    },
    "tv": {
        "name": "テレビ",
        "remote": "tv",
        "commands": {
            "power": {"name": "電源", "key": "KEY_POWER"},
            "vol_up": {"name": "音量+", "key": "KEY_VOLUMEUP"},
            "vol_down": {"name": "音量-", "key": "KEY_VOLUMEDOWN"},
            "ch_up": {"name": "CH+", "key": "KEY_CHANNELUP"},
            "ch_down": {"name": "CH-", "key": "KEY_CHANNELDOWN"},
        },
    },
}

HTML = """
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>リモコン</title>
  <style>
    body { font-family: sans-serif; max-width: 480px; margin: 0 auto; padding: 16px; background: #f5f5f5; }
    h1 { color: #1b4332; font-size: 1.4em; }
    .device { background: #fff; border-radius: 12px; padding: 16px; margin: 12px 0; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
    .device h2 { margin: 0 0 12px 0; font-size: 1.1em; color: #2d6a4f; }
    .buttons { display: flex; flex-wrap: wrap; gap: 8px; }
    .btn { padding: 12px 20px; border: none; border-radius: 8px; background: #2d6a4f; color: #fff; font-size: 1em;
           cursor: pointer; transition: background 0.2s; }
    .btn:active { background: #1b4332; }
    .status { text-align: center; color: #999; font-size: 0.85em; margin-top: 8px; }
  </style>
</head>
<body>
  <h1>IR リモコン</h1>
  {% for dev_id, dev in devices.items() %}
  <div class="device">
    <h2>{{ dev.name }}</h2>
    <div class="buttons">
      {% for cmd_id, cmd in dev.commands.items() %}
      <button class="btn" onclick="send('{{ dev_id }}', '{{ cmd_id }}')">{{ cmd.name }}</button>
      {% endfor %}
    </div>
  </div>
  {% endfor %}
  <div class="status" id="status"></div>
  <script>
    async function send(device, command) {
      const el = document.getElementById('status');
      el.textContent = '送信中...';
      const res = await fetch(`/api/send/${device}/${command}`);
      const data = await res.json();
      el.textContent = data.status === 'ok' ? '送信完了' : 'エラー: ' + data.message;
      setTimeout(() => el.textContent = '', 3000);
    }
  </script>
</body>
</html>
"""

@app.route("/")
def index():
    return render_template_string(HTML, devices=DEVICES)

@app.route("/api/send/<device>/<command>")
def send_command(device, command):
    if device not in DEVICES:
        return jsonify({"status": "error", "message": "Unknown device"}), 404
    dev = DEVICES[device]
    if command not in dev["commands"]:
        return jsonify({"status": "error", "message": "Unknown command"}), 404

    remote = dev["remote"]
    key = dev["commands"][command]["key"]

    result = subprocess.run(
        ["irsend", "SEND_ONCE", remote, key],
        capture_output=True, text=True,
    )

    if result.returncode == 0:
        return jsonify({"status": "ok", "device": device, "command": command})
    return jsonify({"status": "error", "message": result.stderr}), 500

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)

実行

python ir_server.py

スマホのブラウザで http://raspberrypi.local:5000 にアクセスすると、リモコン画面が表示されます。

STEP 6: 自動起動を設定する

sudo nano /etc/systemd/system/ir-remote.service
[Unit]
Description=IR Remote Server
After=network.target

[Service]
ExecStart=/home/pi/ir-remote/bin/python /home/pi/ir_server.py
WorkingDirectory=/home/pi
Restart=always
User=pi

[Install]
WantedBy=multi-user.target
sudo systemctl enable ir-remote
sudo systemctl start ir-remote

STEP 7: 外出先からアクセスする(任意)

家庭内 LAN の外からアクセスするには、以下の方法があります。

方法A: Tailscale(おすすめ)

Tailscale は無料で使える VPN サービスです。Raspberry Pi とスマホの両方にインストールするだけで、外出先から安全にアクセスできます。

# Raspberry Pi にインストール
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up

スマホにも Tailscale アプリをインストールして同じアカウントでログインすれば、Tailscale が割り当てた IP アドレス経由でアクセスできます。

方法B: cron でタイマー操作

外出先アクセスが不要な場合は、cron で時間指定の自動操作が便利です。

crontab -e
# 平日19:00にエアコンON(帰宅前)
0 19 * * 1-5 curl -s http://localhost:5000/api/send/aircon/power

# 毎日23:00に照明OFF(消し忘れ防止)
0 23 * * * curl -s http://localhost:5000/api/send/light/off

エアコン信号の記録(補足)

エアコンのリモコンは温度・モードなどの情報を含む長い信号を送るため、irrecord では記録が難しいことがあります。その場合は生データを使います。

# 生データで記録
mode2 -d /dev/lirc0 > aircon_on.txt
# → リモコンの「冷房26度」ボタンを押す → Ctrl+C

mode2 -d /dev/lirc0 > aircon_off.txt
# → リモコンの「停止」ボタンを押す → Ctrl+C

記録した生データは ir-ctl コマンドで送信できます。

ir-ctl -d /dev/lirc0 --send=aircon_on.txt

トラブルシューティング

| 症状 | 原因と対処 | |------|-----------| | 受信で何も表示されない | VS1838B の配線(OUT/GND/VCC)を確認。GPIO 番号が config.txt と一致しているか確認 | | 送信しても家電が反応しない | 赤外線 LED の向きを確認。スマホカメラで LED が光っているか確認。距離を近づけてテスト | | 「hardware does not support sending」 | config.txt の gpio-ir-tx が正しく設定されているか確認。再起動を試す | | エアコンの信号が記録できない | irrecord の代わりに mode2 で生データを記録する方法を使う |


市販のスマートリモコンは数千円しますが、Raspberry Pi なら数百円のパーツで同じことが実現でき、しかも自由にカスタマイズできます。帰宅前のエアコン起動だけでも、毎日の快適さが変わりますよ。