2026年5月21日

2026年5月21日

WordPressでIoTダッシュボードを構築する方法(センサーデータの受信・保存・グラフ表示)

WordPressでIoTダッシュボードを作るときは、センサーと直接つながろうとせず、「MQTTブローカーなどの外部の仕組みがデータを集め、WordPressは認証つきのREST APIで受け取って保存し、グラフで見せる」という役割分担にするのが現実的です。この記事では、データ用のカスタムテーブル作成、register_rest_route() による受信・取得エンドポイント、Chart.js でのグラフ表示、更新方式の選び方、セキュリティの注意点までを、そのまま試せるコードつきで説明します。

構成の全体像

センサーからグラフまでの流れは次のようになります。

  1. センサー(デバイス)が測定値をMQTTブローカーに送る
  2. ブリッジ(Node-REDや小さなスクリプトなど)がブローカーからデータを受け取り、まとめてWordPressのREST APIへ送る
  3. WordPressがデータを検証し、専用のテーブルに保存する
  4. ダッシュボードのページで、JavaScriptがREST APIからデータを取り出してグラフを描く

デバイスから直接WordPressへ送らない理由は3つあります。1つ目は、PHPのWordPressはリクエストごとに起動して終わる仕組みで、MQTTのような常時接続を保つのに向いていないことです。2つ目は、デバイスにWordPressの認証情報を持たせると、1台盗まれただけでサイトへの書き込み権限が漏れることです。3つ目は、数秒おきの送信がそのままWordPressに届くと、PHPとデータベースの負荷が大きくなることです。ブリッジで1分ごとにまとめて送るだけでも、負荷は大きく下がります。

以下のコードは、1つの自作プラグイン(例: wp-content/plugins/my-iot-dashboard/my-iot-dashboard.php)としてまとめる前提です。テーブルを作成するので、試す前にデータベースのバックアップを取り、できればステージング環境で動かしてください。

測定値を保存するカスタムテーブルを作る

測定値を投稿やカスタムフィールドに入れると、件数が増えたときに wp_postmeta が膨らみ、期間を指定した検索も遅くなります。時系列のデータは専用のテーブルに入れるのが扱いやすい方法です。

<?php
/**
 * Plugin Name: My IoT Dashboard
 */
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

function myiot_table_name() {
	global $wpdb;
	return $wpdb->prefix . 'iot_readings';
}

register_activation_hook( __FILE__, 'myiot_activate' );
function myiot_activate() {
	global $wpdb;
	require_once ABSPATH . 'wp-admin/includes/upgrade.php';

	$table   = myiot_table_name();
	$charset = $wpdb->get_charset_collate();

	$sql = "CREATE TABLE {$table} (
  id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  device_id varchar(64) NOT NULL,
  metric varchar(32) NOT NULL,
  value double NOT NULL,
  recorded_at datetime NOT NULL,
  PRIMARY KEY  (id),
  KEY device_time (device_id, recorded_at)
) {$charset};";

	dbDelta( $sql );

	// 書き込み専用ロールと、閲覧用の権限を用意する
	add_role( 'iot_ingest', 'IoT Ingest', array( 'read' => true, 'myiot_write' => true ) );
	$admin = get_role( 'administrator' );
	if ( $admin ) {
		$admin->add_cap( 'myiot_read' );
	}
}

dbDelta() はSQLの書き方に厳しく、PRIMARY KEY の後ろに半角スペースを2つ入れる、各列を1行ずつ書く、といった約束があります。崩すとテーブルが作られない、または有効化のたびに変更が繰り返されることがあります。有効化の後、phpMyAdminなどで wp_iot_readings(接頭辞はサイトによって異なります)ができているか確認してください。

権限は「書き込み」と「閲覧」を分けています。ブリッジ用のユーザーには iot_ingest ロールだけを与え、管理者には閲覧権限を付けます。

受信と取得のRESTエンドポイントを登録する

次に、ブリッジがデータを送るPOSTと、グラフが読み出すGETを register_rest_route() で登録します。重要なのは permission_callback で権限を確かめること、そして args で受け付ける値の型や範囲を決めておくことです。

add_action( 'rest_api_init', 'myiot_register_routes' );
function myiot_register_routes() {
	register_rest_route(
		'myiot/v1',
		'/readings',
		array(
			array(
				'methods'             => 'POST',
				'callback'            => 'myiot_create_reading',
				'permission_callback' => function () {
					return current_user_can( 'myiot_write' );
				},
				'args'                => array(
					'device_id' => array(
						'type'              => 'string',
						'required'          => true,
						'maxLength'         => 64,
						'validate_callback' => 'rest_validate_request_arg',
						'sanitize_callback' => 'sanitize_key',
					),
					'metric'    => array(
						'type'              => 'string',
						'required'          => true,
						'enum'              => array( 'temperature', 'humidity' ),
						'validate_callback' => 'rest_validate_request_arg',
					),
					'value'     => array(
						'type'              => 'number',
						'required'          => true,
						'validate_callback' => 'rest_validate_request_arg',
					),
				),
			),
			array(
				'methods'             => 'GET',
				'callback'            => 'myiot_get_readings',
				'permission_callback' => function () {
					return current_user_can( 'myiot_read' );
				},
				'args'                => array(
					'device_id' => array(
						'type'              => 'string',
						'required'          => true,
						'validate_callback' => 'rest_validate_request_arg',
						'sanitize_callback' => 'sanitize_key',
					),
					'hours'     => array(
						'type'              => 'integer',
						'default'           => 24,
						'minimum'           => 1,
						'maximum'           => 168,
						'validate_callback' => 'rest_validate_request_arg',
					),
				),
			),
		)
	);
}

function myiot_create_reading( WP_REST_Request $request ) {
	global $wpdb;

	// 簡易的な上限: 書き込みが続く間は回数を数え、1分間途切れるとリセットされる
	$key   = 'myiot_rate_' . get_current_user_id();
	$count = (int) get_transient( $key );
	if ( $count >= 120 ) {
		return new WP_Error( 'myiot_rate_limited', 'Too many requests.', array( 'status' => 429 ) );
	}
	set_transient( $key, $count + 1, MINUTE_IN_SECONDS );

	$ok = $wpdb->insert(
		myiot_table_name(),
		array(
			'device_id'   => $request['device_id'],
			'metric'      => $request['metric'],
			'value'       => (float) $request['value'],
			'recorded_at' => current_time( 'mysql', true ), // UTCで保存する
		),
		array( '%s', '%s', '%f', '%s' )
	);

	if ( false === $ok ) {
		return new WP_Error( 'myiot_db_error', 'Could not save the reading.', array( 'status' => 500 ) );
	}
	return new WP_REST_Response( array( 'id' => (int) $wpdb->insert_id ), 201 );
}

function myiot_get_readings( WP_REST_Request $request ) {
	global $wpdb;
	$table = myiot_table_name();
	$since = gmdate( 'Y-m-d H:i:s', time() - HOUR_IN_SECONDS * (int) $request['hours'] );

	$rows = $wpdb->get_results(
		$wpdb->prepare(
			"SELECT metric, value, recorded_at FROM {$table}
			 WHERE device_id = %s AND recorded_at >= %s
			 ORDER BY recorded_at ASC LIMIT 5000",
			$request['device_id'],
			$since
		),
		ARRAY_A
	);
	return rest_ensure_response( $rows );
}

permission_callback を __return_true にすると、誰でもデータを書き込めるエンドポイントになってしまいます。WordPress 5.5以降はこの引数を省略すると注意が出るようになっており、省略やその場しのぎの __return_true は避けてください。SQLは $wpdb->prepare() で値を埋め込み、保存は $wpdb->insert() に型指定つきで渡しています。

上の回数制限はトランジェントを使った簡易的なもので、厳密な「1分あたり何回」ではありません。本格的な制限は、Webサーバーやリバースプロキシ、WAFの機能で行うほうが確かです。

ブリッジからの送信とアプリケーションパスワード

ブリッジの認証には、WordPress 5.6で追加されたアプリケーションパスワードが使えます。ブリッジ用のユーザーを iot_ingest ロールで作り、管理画面の「ユーザー」→該当ユーザーのプロフィール画面でアプリケーションパスワードを発行します。アプリケーションパスワードは、通常はHTTPS接続のサイトでだけ有効になります。

# ブリッジ側からの送信例(パスワードはダミー。実際は環境変数などから読む)
curl -X POST "https://example.com/wp-json/myiot/v1/readings" \
  -u "iot-bridge:your-application-password" \
  -H "Content-Type: application/json" \
  -d '{"device_id":"room-1","metric":"temperature","value":23.4}'

成功するとHTTP 201と保存したIDが返ります。401や403が返る場合は、ユーザー名とアプリケーションパスワードの組み合わせ、ロールの権限、サーバーが Authorization ヘッダーを PHP に渡しているかを確認します。一部のサーバー環境では、このヘッダーが途中で落とされるために認証が通らないことがあります。

ダッシュボードにグラフを表示する

表示側は、ショートコードで <canvas> を出し、Chart.js と自作スクリプトを wp_enqueue_script() で読み込みます。Chart.js の配布ファイル(UMD版)はプラグイン内の assets/ に置く想定です。外部CDNから読む場合も、<script> タグを直書きせず同じ関数で登録します。

add_shortcode( 'iot_chart', 'myiot_chart_shortcode' );
function myiot_chart_shortcode( $atts ) {
	if ( ! current_user_can( 'myiot_read' ) ) {
		return '';
	}
	$atts   = shortcode_atts( array( 'device' => '' ), $atts, 'iot_chart' );
	$device = sanitize_key( $atts['device'] );

	// バージョン文字列は、置いたChart.jsのバージョンに合わせる
	wp_enqueue_script( 'chartjs', plugins_url( 'assets/chart.umd.js', __FILE__ ), array(), '4.4.0', true );
	wp_enqueue_script( 'myiot-chart', plugins_url( 'assets/iot-chart.js', __FILE__ ), array( 'chartjs' ), '1.0.0', true );
	wp_add_inline_script(
		'myiot-chart',
		'window.myIot = ' . wp_json_encode(
			array(
				'endpoint' => rest_url( 'myiot/v1/readings' ),
				'nonce'    => wp_create_nonce( 'wp_rest' ),
			)
		) . ';',
		'before'
	);

	return '<canvas class="myiot-chart" data-device="' . esc_attr( $device ) . '" height="120"></canvas>';
}
// assets/iot-chart.js
document.querySelectorAll('.myiot-chart').forEach(async (canvas) => {
  const url = new URL(window.myIot.endpoint);
  url.searchParams.set('device_id', canvas.dataset.device);
  url.searchParams.set('hours', '24');

  const chart = new Chart(canvas, {
    type: 'line',
    data: { labels: [], datasets: [{ label: '温度', data: [] }] },
  });

  async function refresh() {
    const res = await fetch(url, {
      headers: { 'X-WP-Nonce': window.myIot.nonce },
      credentials: 'same-origin',
    });
    if (!res.ok) return;
    const rows = await res.json();
    const temps = rows.filter((r) => r.metric === 'temperature');
    chart.data.labels = temps.map((r) => r.recorded_at);
    chart.data.datasets[0].data = temps.map((r) => Number(r.value));
    chart.update();
  }

  await refresh();
  setInterval(refresh, 60000); // 60秒ごとに再取得
});

ログイン中のユーザーがブラウザからREST APIを呼ぶときは、Cookieに加えて X-WP-Nonce ヘッダーで wp_rest のnonceを送る必要があります。これがないと、ログインしていても権限のないユーザーとして扱われます。ページを長時間開いたままにするとnonceの有効期限が切れて403が返るようになるため、ダッシュボードを常時表示する用途では、エラー時に再読み込みを促す表示を入れておくと親切です。

更新方式の選び方: ポーリングかプッシュか

上の例は一定間隔で取りに行く「ポーリング」です。実装が単純で、キャッシュや認証の仕組みもそのまま使えるため、1分前後の更新で足りるならこれで十分です。

秒単位で値が動く表示が必要な場合は、Server-Sent EventsやWebSocketでサーバーから送る方式が候補になります。ただし、PHPで接続を開いたままにすると、その間PHPの処理枠を1つ占有し続けます。共用サーバーでは同時接続数がすぐ上限に達するため、リアルタイム配信は専用のサービスや別のサーバーに任せ、WordPressはその画面を埋め込むだけにする構成のほうが安定します。

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

  • デバイスやブリッジの認証情報を、ページのHTMLやJavaScriptに書かない。ブラウザに渡すのはnonceだけにする
  • ブリッジ用ユーザーは書き込み権限だけを持つ専用ユーザーにし、管理者アカウントを使わない
  • 通信はHTTPSに限定し、不要になったアプリケーションパスワードは失効させる
  • 閲覧ページは権限チェックで守り、公開ページにする場合も取得できる期間や件数に上限を設ける
  • 古いデータは wp_schedule_event() で定期的に削除するなど、保存期間を決めてテーブルの肥大化を防ぐ

WordPressのバージョンやプラグインの対応状況は変わることがあるため、本番に入れる前に、利用中の環境で動作を確かめてください(2026年10月現在)。

まとめ

  • デバイスとの常時接続はMQTTブローカーとブリッジに任せ、WordPressはREST APIで受け取る役に徹する
  • 時系列データは dbDelta() で作る専用テーブルに保存し、権限は書き込みと閲覧で分ける
  • register_rest_route() では permission_callback と args の検証を省かない
  • グラフは wp_enqueue_script() で読み込み、X-WP-Nonce 付きの fetch でデータを取得する
  • 更新はまずポーリングで作り、リアルタイム性が必要な部分だけ外部の仕組みに任せる

お気軽にご相談ください

お見積りへ お問い合わせへ
▲