この手順の目次
開始条件

SH-02Eのアプリから家電を操作でき、Wi-Fi ADBと中継APIが稼働していること。中継はLinuxホストの127.0.0.1:8766で待ち受ける。以下の配布ファイルはHome Assistant側の接続例で、端末初期化・アプリ・中継サーバーそのもののインストーラーではありません。

1. 設定例を取得する

このページは新規のテスト用Home Assistantを使う例。既存のHome Assistantが動いている場合、同じ8123番で二つ起動せず、rest_commandとscriptなど必要な項目だけを既存設定へ統合する。公式のLinux導入手順でDocker Engineを準備しておく。

BASH · Linuxサーバー
mkdir -p ir-lab/ha-config
cd ir-lab
curl -fSLo compose.yaml \
  https://hacks.hacci.dev/sh-02e-ir-remote/examples/compose.example.yaml
curl -fSLo ha-config/configuration.yaml \
  https://hacks.hacci.dev/sh-02e-ir-remote/examples/configuration.example.yaml
curl -fSLo ha-config/ui-lovelace.yaml \
  https://hacks.hacci.dev/sh-02e-ir-remote/examples/ui-lovelace.example.yaml

# 実行前にファイル内容を読む
cat compose.yaml
cat ha-config/configuration.yaml
cat ha-config/ui-lovelace.yaml
docker compose version

配布ファイルのSHA-256一覧も用意した。ファイル内容を読んでから使用する。

コンテナはLinuxのhost networkを使うため、コンテナからの127.0.0.1が中継ホストと一致する。Docker Desktopや別ホストの中継へ、同じ前提を当てはめない。サーバーには空いている8123番が必要。

compose.yamlの内容
YAML
# Linux Docker Engine. Requires an existing relay on host loopback:8766.
# This example does not install the Android app, kernel module or relay.
name: ir-lab-ha
services:
  homeassistant:
    image: ghcr.io/home-assistant/home-assistant:2026.9.4
    restart: unless-stopped
    network_mode: host
    environment:
      TZ: Asia/Tokyo
    volumes:
      - ./ha-config:/config
    security_opt:
      - no-new-privileges:true
    mem_limit: 2g
    logging:
      driver: json-file
      options:
        max-size: 10m
        max-file: '3'

2. 自分のIP・トークン・機器IDを設定する

中継APIの既存トークンと、LAN上のサーバー自身のIPを入力する。ここで新しいトークンを勝手に生成すると、稼働中の中継と一致しない。機器IDはAndroidアプリと中継の両方が許可した値を使う。

BASH · 値の設定
umask 077
read -rp 'Server LAN IP (bind address): ' IR_HA_IP
read -rsp 'Existing relay token: ' IR_RELAY_TOKEN; echo
read -rp 'Registered light device ID: ' IR_LIGHT_ID
export IR_HA_IP IR_RELAY_TOKEN IR_LIGHT_ID
python3 - <<'CONFIG'
import ipaddress, json, os, re
from pathlib import Path
ip = str(ipaddress.ip_address(os.environ['IR_HA_IP']))
token = os.environ['IR_RELAY_TOKEN']
if not re.fullmatch(r'[0-9a-f]{64}', token):
    raise SystemExit('Expected the existing 64-character relay token')
identifier = os.environ['IR_LIGHT_ID']
if not re.fullmatch(r'[A-Za-z0-9_-]+', identifier):
    raise SystemExit('Invalid device ID')
p = Path('ha-config/secrets.yaml')
p.write_text('ha_bind_ip: ' + json.dumps(ip) + '\n' +
             'ir_relay_authorization: ' + json.dumps('Bearer ' + token) + '\n')
p.chmod(0o600)
p = Path('ha-config/ui-lovelace.yaml')
p.write_text(p.read_text().replace('REGISTERED_LIGHT_ID', identifier))
CONFIG
unset IR_RELAY_TOKEN
# secrets.yamlの内容を画面に出したり、公開ディレクトリへ置かない
ls -l ha-config/secrets.yaml

機器IDを誤った場合、表示名だけが正しくても送信は400で拒否される。このサンプルには家庭内の実IP・実トークン・部屋名を含めていない。

3. RESTコマンドとスクリプトの役割

rest_commandが中継へJSONを送り、script.ir_sendが失敗応答をエラーとして扱う。照明には温度を付けず、cool / heatだけに整数温度を付ける。端末アプリ側でも値を検証する。

YAML · configuration.yaml
# New Home Assistant test instance only. Merge sections into existing installations.
# Prerequisite: initialized SH-02E, registered device IDs, and a running relay on this host.
homeassistant:
  name: IR Lab
  time_zone: Asia/Tokyo
  unit_system: metric
frontend:
config:
http:
  server_host: !secret ha_bind_ip
  server_port: 8123
lovelace:
  mode: yaml
rest_command:
  ir_relay_send:
    url: http://127.0.0.1:8766/send
    method: POST
    headers:
      Authorization: !secret ir_relay_authorization
    content_type: application/json
    payload: >-
      {% if command in ['cool', 'heat'] %}
      {{ dict(device=device, command=command, temperature=temperature | int) | to_json }}
      {% else %}
      {{ dict(device=device, command=command) | to_json }}
      {% endif %}
    timeout: 12
script:
  ir_send:
    alias: IR signal send
    mode: parallel
    max: 10
    sequence:
      - action: rest_command.ir_relay_send
        data:
          device: "{{ device }}"
          command: "{{ command }}"
          temperature: "{{ temperature | default(26) }}"
        response_variable: reply
      - if: "{{ reply.status | default(0) | int != 200 }}"
        then:
          - stop: "IR send failed or outcome unknown. Not retried."
            error: true

4. 設定を検証して起動する

BASH · サーバー
# 先に設定検証する。コンテナの起動・公開はまだ行わない
docker compose run --rm --no-deps homeassistant \
  python -m homeassistant --script check_config --config /config
# 成功したら起動する
docker compose up -d
docker compose ps
docker compose logs --tail=50 homeassistant

同じLANのブラウザーでhttp://<SERVER_LAN_IP>:8123/を開き、初回だけローカルアカウントを作る。ダッシュボードに点灯/全灯・常夜灯・消灯が表示される。実際に送信する前に、次の準備確認を行う。

5. APIの準備状態を確認する

BASH · 無送信
# 中継ホスト上で実行。トークンを再入力する
read -rsp 'Relay token: ' IR_RELAY_TOKEN; echo
export IR_RELAY_TOKEN
curl --fail-with-body --max-time 20 \
  -H "Authorization: Bearer ${IR_RELAY_TOKEN}" \
  http://127.0.0.1:8766/health
JSON · 初回の応答例
{"ready":true,"busy":false,"last_sent":null}

200であってもready=falseなら端末準備が整っていない。busy=trueなら別処理中で新しい端末確認を省略している。last_sentがnull以外でも、家電の現在の状態を示すものではない。

6. まず1つの信号を送る

SH-02Eの背面赤外線ポートを家電へ向ける。次のコマンドは実際の送信を伴う。IR_LIGHT_IDは手順2で入力した値である。

BASH · 常夜灯の送信
IR_BODY=$(python3 -c 'import json,os; print(json.dumps({"device":os.environ["IR_LIGHT_ID"],"command":"nightlight"}))')
curl --fail-with-body --max-time 15 \
  -X POST http://127.0.0.1:8766/send \
  -H "Authorization: Bearer ${IR_RELAY_TOKEN}" \
  -H 'Content-Type: application/json' \
  --data "$IR_BODY"
unset IR_RELAY_TOKEN IR_BODY
JSON · 送信成功時
{"sent":true,"appliance_response_verified":false}

確認は「APIが成功した」と「家電が常夜灯になった」の2段階で記録する。400は入力、401は認証、409は同時実行、503は失敗または結果不明。503やタイムアウトを見て自動再試行しない。

7. Web UIでボタンとして操作する

次のカードはオン・オフの状態を持たず、押すたびに操作する。常夜灯にしてから、別の操作をしても表示をリセットする必要がない。AlexaのHue互換トグルとは異なり、Home AssistantではこのボタンUIを直接使える。

ダッシュボードの3ボタン設定
YAML · ui-lovelace.yaml
# Replace REGISTERED_LIGHT_ID with the ID allowed by your relay and Android app.
title: IR Lab
views:
  - title: リモコン
    path: remote
    cards:
      - type: markdown
        content: >-
          ボタンは信号を1回実行します。家電の実際の状態は検知しません。
      - type: grid
        columns: 3
        square: false
        cards:
          - type: button
            name: 点灯/全灯
            icon: mdi:lightbulb-on
            show_state: false
            tap_action:
              action: perform-action
              perform_action: script.ir_send
              data:
                device: REGISTERED_LIGHT_ID
                command: 'on'
            hold_action: {action: none}
          - type: button
            name: 常夜灯
            icon: mdi:weather-night
            show_state: false
            tap_action:
              action: perform-action
              perform_action: script.ir_send
              data:
                device: REGISTERED_LIGHT_ID
                command: nightlight
            hold_action: {action: none}
          - type: button
            name: 消灯
            icon: mdi:lightbulb-off
            show_state: false
            tap_action:
              action: perform-action
              perform_action: script.ir_send
              data:
                device: REGISTERED_LIGHT_ID
                command: 'off'
            hold_action: {action: none}

機器IDはダウンロード後に設定した値へ置き換わる。ボタンを増やすなら、同じ形でcommandをmemory1、brightness_upなど許可済みの操作に変更する。エアコンでは入力値を変更しただけで送信せず、最後に送信ボタンを押す設計にする。

8. 別プログラムからHome Assistant APIを使う

Home Assistantのプロフィールで長期アクセストークンを作成する。中継トークンとは別物。APIの詳細は公式REST API資料を参照。

BASH · HA APIから送信
read -rp 'Home Assistant base URL: ' HA_BASE_URL
read -rsp 'Home Assistant access token: ' HA_TOKEN; echo
# 例: 信頼するLAN内の http://<SERVER_LAN_IP>:8123
IR_BODY=$(python3 -c 'import json,os; print(json.dumps({"device":os.environ["IR_LIGHT_ID"],"command":"nightlight"}))')
curl --fail-with-body --max-time 20 \
  -X POST "${HA_BASE_URL%/}/api/services/script/ir_send" \
  -H "Authorization: Bearer ${HA_TOKEN}" \
  -H 'Content-Type: application/json' \
  --data "$IR_BODY"
unset HA_TOKEN IR_BODY

Home Assistantの応答は中継のsent JSONとは異なり、変更された状態のリストなどが返る。/api/states/...へonと書き込むだけでは家電操作にならない。スクリプトのサービスを呼ぶ。

9. 自作Web UIと端末内HTTPサーバー

自作画面なら、ブラウザー → ログイン管理する自作バックエンド → 中継APIという順に接続する。中継トークンはバックエンドだけで保持する。ブラウザーの127.0.0.1は閲覧端末自身なので、中継のループバックへ直接fetchする実装にはしない。

SH-02Eの中にHTTPサーバーを置く方式は未実装の拡張案。API16対応のHTTP実装、INTERNET権限、認証、送信排他、foreground service、CPU / Wi-Fiロックが必要になる。今回のダウンロード例はその実装ではない。

技術記事へ戻る →次:運用・再起動・トラブルシュート →