2026年5月21日
2026年5月21日
WordPressでIoTダッシュボードを構築する方法(センサーデータの受信・保存・グラフ表示)
WordPressでIoTダッシュボードを作るときは、センサーと直接つながろうとせず、「MQTTブローカーなどの外部の仕組みがデータを集め、WordPressは認証つきのREST APIで受け取って保存し、グラフで見せる」という役割分担にするのが現実的です。この記事では、データ用のカスタムテーブル作成、register_rest_route() による受信・取得エンドポイント、Chart.js でのグラフ表示、更新方式の選び方、セキュリティの注意点までを、そのまま試せるコードつきで説明します。
構成の全体像
センサーからグラフまでの流れは次のようになります。
- センサー(デバイス)が測定値をMQTTブローカーに送る
- ブリッジ(Node-REDや小さなスクリプトなど)がブローカーからデータを受け取り、まとめてWordPressのREST APIへ送る
- WordPressがデータを検証し、専用のテーブルに保存する
- ダッシュボードのページで、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でデータを取得する - 更新はまずポーリングで作り、リアルタイム性が必要な部分だけ外部の仕組みに任せる