SWPF_AUTOMATION_処理とデータ流れ解説_サンプル付き

← 一覧へ戻る SWPF_AUTOMATION_処理とデータ流れ解説_サンプル付き

SWPF AUTOMATION 設定と実コードの動き・データの流れ解説

対象読者: ロジック経験はあるが、Drupal / サーバー側フレームワークにはまだ慣れていない開発者向け 前提: この説明は、今回確認した実コードをベースに、「何が起点で、どの順番で、どのデータが、どこへ渡るか」を追えるように整理したものです。


1. まず全体像

SWPF AUTOMATION は、ざっくり言うと次の4層で動きます。

  1. トリガ層

CRON や LINE受信、SwitchBot受信、Kintone受信など、起動のきっかけを決める層

  1. TriggerEntity 層

「このユーザーは、このトリガで、どの GROUP_ID を動かすか」を管理する層

  1. TemplateGroup / TemplateEntity 層

GROUP_ID に属する複数の TEMPLATEENTITY を、weight 順に実行する層

  1. Executor / Plugin 層

各 TEMPLATEENTITY の PARAMSJSON / RULEJSON / TEMPLATE_JSON を解釈して、 実際のプラグイン呼び出し・式計算・固定値投入を行う層

この関係を先に図で見るとこうです。

flowchart TD A[トリガ発生<br/>CRON / LINE / MQTT / SwitchBot / Kintone] --> B[SwpfAutomationFireService] B --> C[TriggerEntity を検索<br/>trigger_type + enabled + uid] C --> D[TriggerEntityごとに GROUP_ID を取得] D --> E[SwpfAutomationGroupRunner] E --> F[GROUP_ID配下の TEMPLATEENTITY を weight順に取得] F --> G[SwpfAutomationExecutor] G --> H[PARAMS_MAP 作成] H --> I[RULE_JSON 評価] I --> J[TEMPLATE_JSON から template_id を選択] J --> K{template.type} K -->|COMMAND| L[プラグイン実行] K -->|EXPRESSION| M[input.results に結果格納] K -->|LITERAL| N[input.results に固定値格納] L --> O[context/input.raws/input.results 更新] M --> O N --> O O --> P[次の TEMPLATEENTITY へ引き継ぎ]

2. 重要な考え方

この仕組みを理解するポイントは次の3つです。

2-1. トリガは「処理そのもの」ではなく「起動ポイント」

トリガ定数は、どの処理を呼ぶかを直接書いたものではありません。 「この名前のイベントが起きた」という入口です。

実コード上は swpfcommon 側のトリガ定数を swpfautomation から互換参照しています。

  • CRON1M, CRON5M, CRON_15M など
  • LINE_RECEIVED
  • KINTONE_RECEIVED
  • SWITCHBOT_RECEIVED
  • 環境によっては MQTT_RECEIVED のようなイベントトリガも使用

つまり、トリガは「関数名」ではなく、イベント名 / 発火名です。

2-2. TriggerEntity は「どの GROUP_ID を起動するか」の対応表

ユーザーは TriggerEntity によって、

  • どの trigger_type
  • どの templategroupid
  • 有効化するか

を設定します。

つまり TriggerEntity は、

イベント名 → 実行グループ

の対応表です。

2-3. TemplateEntity は「単一ステップ」、GROUP_ID は「一連の流れ」

1個の TEMPLATEENTITY は、1つの判断・1つの動作単位です。 ただし実際には RULEJSON により複数 templateid が選ばれたり、template_sequence が組まれたりするので、 1テンプレート = 1行の命令というより、1ステップ分のロジック定義と考えると分かりやすいです。

GROUP_ID は、それを複数まとめて 順番に流すパイプラインです。


3. 起動の入口

3-1. CRON 系トリガの入口

CRON は swpfautomation.modulehookcron() から入ります。

処理の流れはこうです。

sequenceDiagram participant Cron as Drupal Cron participant Mod as swpf_automation_cron() participant CTM as CronTriggerManager participant Fire as SwpfAutomationFireService Cron->>Mod: hook_cron 実行 Mod->>CTM: 今回発火すべき CRON_xx を取得 CTM-->>Mod: should_fire=true な trigger 一覧 loop triggerごと Mod->>Fire: fireBatch(trigger, rawData=[], context=cron情報) end

ここで重要なのは、CRON 自体は直接 Template を実行しないことです。 CRON はまず 「今回発火すべきトリガ名」 を出し、そこから fireBatch() に渡します。

つまり、CRON 側は

  • 時間管理
  • 二重起動防止
  • 発火タイミング計算

を担当し、実際の自動化ロジックは FireService 側に渡します。


3-2. イベント系トリガの入口

イベント系は、プラグインの中から fire() が呼ばれることがあります。

例えば実コードでは次のような流れがあります。

  • FetchRawDataFromMqttQueue が MQTT RAW を取得して MQTT_RECEIVED を発火
  • FetchRawDataFromKintone が Kintone データを取得して KINTONE_RECEIVED を発火
  • FetchSensorDataFromSwitchbot が SwitchBot データ取得後に SWITCHBOT_RECEIVED を発火

つまり SWPF AUTOMATION では、

  • 外側のCRONから直接起動する流れ
  • いったんプラグインで外部データを取得し、その中で次のトリガを起こす流れ

の両方があります。

これを図にするとこうです。

flowchart LR A["CRON_15M 発火"] --> B["GROUP A 実行"] B --> C["Kintone取得プラグイン実行"] C --> D["外部からKintone取得"] D --> E["KINTONE_RECEIVED を fire"] E --> F["別の TriggerEntity が反応"] F --> G["GROUP B 実行"]

ここで大事なのは、プラグイン内 fire も通常の GROUP 実行と同じく context/raws/results を持ったまま次の流れへ渡せるということです。


4. FireService の役割

FireService はこの仕組みの「交通整理係」です。

4-1. fireBatch(triggerType, rawData, options)

これは主に CRON 用です。 指定された triggerType に一致する TriggerEntity を全部拾い、対象ユーザーごとに fire() を呼びます。

ポイント:

  • trigger_type が一致する TriggerEntity を検索
  • enabled=1 のものだけ対象
  • 通常は 同じ uid について重複発火しないように整理
  • ユーザーごとの rawData 上書きも可能

つまり fireBatch() は、

このトリガを受ける人たちを探して、それぞれに個別 fire する

という役目です。

4-2. fire(uid, triggerType, rawData, options)

こちらが実際の本体です。

処理のイメージ:

flowchart TD A["fire を受け取る"] --> B["TriggerEntity を検索"] B --> C["uid と triggerType が一致するものを weight順で取得"] C --> D["各 TriggerEntity から template_group_id を取得"] D --> E["GroupRunner を実行"] E --> F["結果一覧を返却"]

ここで重要なのは、1つの triggerType に対して TriggerEntity が複数あってもよいということです。 つまり同じトリガで複数 GROUP_ID を起動できます。

ただしそのとき、GROUP_ID が異なればデータは共有されません。

これはかなり重要です。

  • 同じ GROUP_ID 内: データは引き継がれる
  • 別 GROUP_ID: それぞれ独立した実行になる

5. GROUP_ID 単位の処理

GROUP の実行は SwpfAutomationGroupRunner::run() が担当します。

ここが、あなたの言う

GROUPID は TEMPLATEENTITY(一連の動作のテンプレート)を束ねており、TEMPLATEENTITY に設定された実行順序に従って順番に動作する

をそのまま実装している部分です。

5-1. GROUP 内で行うこと

  1. uid + groupidactiontemplate_entity を検索
  2. enabled=1 のみ対象
  3. weight ASC, id ASC で順番確定
  4. 1件ずつ Executor に渡して実行
  5. 実行後の input.rawsinput.results を次の TEMPLATEENTITY に渡す

図にするとこうです。

flowchart TD A["GroupRunner 開始"] --> B["TemplateEntity 一覧取得"] B --> C["TemplateEntity 1"] C --> D["TemplateEntity 2"] D --> E["TemplateEntity 3"] E --> F["終了"] C -. "更新後の input.raws / input.results を引き継ぐ" .-> D D -. "更新後の input.raws / input.results を引き継ぐ" .-> E

5-2. GROUP 内で引き継がれるもの

GroupRunner が次段へ渡す主なものはこれです。

  • input.raws
  • input.results
  • trace
  • meta

実コード上、特に明示的に引き継いでいるのは次の2つです。

input.raws

前段プラグインや前段評価結果によって更新された、生データ寄りの作業領域

input.results

式計算や固定値投入などの中間結果置き場

この2つがあるので、GROUP は単なる「順番実行」ではなく、前段の出力を後段が参照できる実行パイプラインになります。


6. 1つの TEMPLATEENTITY の中で何が起こるか

ここからが一番重要です。 1つの TEMPLATEENTITY は、Executor に渡されると次の順で処理されます。

flowchart TD A[action_template_entity_id を受け取る] --> B[ContextBuilderで bundle 生成] B --> C[params_json 読み込み] C --> D[params_map 作成] D --> E[rule_json 評価] E --> F[selected_template_id / selected_template_ids 決定] F --> G[template_json から template_sequence 構築] G --> H{type判定} H -->|COMMAND| I[プラグイン実行] H -->|EXPRESSION| J[input.resultsへ保存] H -->|LITERAL| K[input.resultsへ保存] I --> L[pluginが返したcontextをmerge] J --> M[params_map再構築] K --> M L --> N[次のTEMPLATEENTITYへ] M --> N

7. ContextBuilder の役割

Executor はいきなり rule_json を評価するのではなく、まず ContextBuilder で周辺情報を集めます。

7-1. ContextBuilder が集めるもの

主に次を組み立てます。

  • ActionTemplateEntity 本体
  • その Template に紐づく Profile
  • その Profile に紐づく DeviceService
  • params_json
  • rule_json
  • template_json
  • fallback 用 plugin_id

つまり ContextBuilder は、

実行に必要な設定一式を bundle にまとめる担当

です。

7-2. plugin_id の優先関係

コードから読み取れる優先順位はこうです。

  1. templatejson[対象templateid].plugin
  2. templateentity.pluginid
  3. profileentity.pluginid

つまり TEMPLATEJSON の各項目で plugin を明示した場合、それが最優先です。 なければ TemplateEntity の pluginid、それもなければ ProfileEntity の plugin_id が使われます。

これは、

  • 基本は共通プラグインを Profile で持たせる
  • 特定の Template だけ別プラグインにしたいときは TEMPLATE_JSON 側で上書きする

という運用ができるようにするためです。


8. PARAMSJSON と PARAMSMAP の意味

ここはサーバーに不慣れな人が最初につまずきやすい箇所です。

8-1. なぜ PARAMS_JSON が必要か

IoT の生データは、送信元ごとにキー名がバラバラになりがちです。

例:

  • MQTT は temp
  • SwitchBot は temperature
  • Kintone は record.body.temp.value
  • ChatGPT結果は result.score.value

このまま RULE_JSON に直接書くと、テンプレートごとに参照方法がバラバラになります。

そこで PARAMS_JSON で、

  • ルール側で使いたい名前
  • 実際の内部データの場所

を対応付けます。

つまり PARAMS_JSON は、評価用の別名定義です。

8-2. PARAMS_MAP はいつ作られるか

Executor は RULEJSON を評価する前に、必ず paramsmap を作ります。 サービス定義上、この処理は @swpfcommon.paramsmap_builder に委譲されています。

つまり概念上はこうです。

flowchart LR A[input.raws] --> C[params_map_builder] B[params_json] --> C C --> D[input.params_map]

8-3. 何が入るか

params_map には少なくとも次の2系統が入ります。

  1. raw 由来の値

input.raws から PARAMS_JSON に従って取り出された値

  1. result 由来の値

前段の EXPRESSION / LITERAL により input.results に入った値を、result.* プレフィックスで追加した値

実コードでは refreshResultsParamsMap()input.resultsinput.params_map に再反映します。

つまり後段では、たとえば次のような参照が可能になります。

  • TEMP
  • raw.weather.jma.forecast
  • result.score.value
  • result.alert_message.value

この設計の利点は、

生データの場所と、ロジックで使う変数名を分離できる

ことです。


9. RULE_JSON の役割

RULEJSON は、PARAMSMAP を見て どの TEMPLATE_ID を使うか決める部分です。

ここで大事なのは、RULEJSON はプラグインそのものを直接呼ぶのではなく、まず templateid の選択を行う点です。

つまり流れはこうです。

PARAMS_MAP を作る
  ↓
RULE_JSON を評価する
  ↓
選ばれた TEMPLATE_ID を得る
  ↓
TEMPLATE_JSON[そのTEMPLATE_ID] を使って実処理する

9-1. AND / OR / ネスト

ご指摘の通り、RULEJSON は複数ネストに対応し、IF判定で AND / OR をサポートする前提の構造です。 今回確認した Executor は評価自体を @swpfcommon.eval_json に委譲しているため、厳密な演算子実装本体は COMMON 側にありますが、Automation 側の使い方としては以下の理解で問題ありません。

  • RULE_JSON は単純な1回判定だけではない
  • ネストした条件木を持てる
  • 結果として selectedtemplateid または selectedtemplateids が返る

9-2. 複数 TEMPLATE_ID を返せる

Executor は単一の selectedtemplateid だけでなく、selectedtemplateids を前提に template_sequence を組む実装になっています。

つまり RULE_JSON は

  • 1個の template_id を返す場合
  • 複数 template_id を返して、順番に COMMAND / EXPRESSION / LITERAL を並べる場合

の両方に対応できます。


10. TEMPLATE_JSON の役割

TEMPLATEJSON は、選ばれた TEMPLATEID に対して、実際に何をするかを定義します。

ユーザーの整理どおり、ここには「単一動作の手続き」が入ります。

実コード上、type は主に次の3種類です。

  • COMMAND
  • EXPRESSION
  • LITERAL

10-1. COMMAND

COMMAND は最終的にプラグインへ渡される実行命令です。 JSON payload の場合もあれば、文字列テンプレートの場合もあります。

例のイメージ:

{
  "type": "COMMAND",
  "plugin": "line_message_execute",
  "command": {
    "REPLYHEADER": "こんにちは{USER_NAME}さん",
    "REPLY": "現在温度は{TEMP}です"
  }
}

この {TEMP} のようなプレースホルダは、params_map により置換されます。

10-2. EXPRESSION

EXPRESSION は式評価です。 プラグインは実行せず、計算結果を input.results に格納します。

例:

{
  "type": "EXPRESSION",
  "expr": "({TEMP} * 2) + {HUM}",
  "result_key": "score"
}

実行後は概念的にこうなります。

input.results.score = {
  "key": "score",
  "value": 123,
  "type": "EXPRESSION",
  "template_id": "SCORE_CALC"
}

そしてその結果が result.score.value のように params_map に再取り込みされ、次のルールやコマンドから参照できるようになります。

10-3. LITERAL

LITERAL は固定値投入です。 式評価せず、そのまま値を input.results に入れます。

例:

{
  "type": "LITERAL",
  "value": "異常あり",
  "result_key": "alert_message"
}

これは「変数に固定値を設定する」に対応します。


11. 1つの TEMPLATEENTITY の中でのデータ変形

ここはデータの流れだけに絞って見ます。

11-1. 初期入力

GroupRunner から Executor へ渡る時点では概ねこうです。

{
  "input": {
    "raws": { ...前段から来た生データ... },
    "results": { ...前段の中間結果... }
  },
  "action_template_entity_id": 123,
  "meta": { ... },
  "trace": { ... }
}

11-2. ContextBuilder 後

TemplateEntity / Profile / DeviceService 由来の設定が bundle 化されます。

{
  "bundle": {
    "params_json": {...},
    "rule_json": {...},
    "template_json": {...},
    "plugin_id": "fallback_plugin",
    "config_json": {...},
    "capabilities_json": {...}
  }
}

11-3. params_map 作成後

{
  "input": {
    "raws": {...},
    "results": {...},
    "params_map": {
      "TEMP": 27.4,
      "HUM": 45.2,
      "result.score.value": 82
    }
  }
}

11-4. RULE_JSON 評価後

{
  "meta": {
    "selected_template_id": "MAIL_NORMAL",
    "used_template_id": "MAIL_NORMAL",
    "selected_template_ids": ["CALC_SCORE", "MAIL_NORMAL"],
    "used_template_ids": ["CALC_SCORE", "MAIL_NORMAL"]
  }
}

11-5. TEMPLATE_JSON 展開後

{
  "template_sequence": [
    {
      "template_id": "CALC_SCORE",
      "type": "EXPRESSION",
      "expr": "({TEMP}*2)+{HUM}",
      "result_key": "score"
    },
    {
      "template_id": "MAIL_NORMAL",
      "type": "COMMAND",
      "plugin_id": "mail_send_execute",
      "command": {
        "subject": "温度通知",
        "body": "score={result.score.value}"
      }
    }
  ]
}

11-6. 実行後

  • EXPRESSION / LITERAL は input.results を更新
  • COMMAND はプラグインを実行し、その plugin が返した context をマージ
  • 最終 context が次の TEMPLATEENTITY に渡る

12. プラグイン実行時の流れ

COMMAND がある場合、Executor は plugin.manager.swpf_execute からプラグインを生成して実行します。

しかも今の実装は、template_sequence の各 COMMAND を順番に全部実行します。 以前は最後の1件寄りの扱いでしたが、現在は複数 COMMAND の逐次実行になっています。

sequenceDiagram participant Exec as SwpfAutomationExecutor participant PM as plugin.manager.swpf_execute participant P1 as Plugin #1 participant P2 as Plugin #2 Exec->>PM: createInstance(plugin_id_1) PM-->>Exec: Plugin #1 Exec->>P1: execute(context) P1-->>Exec: pluginResult + context Exec->>Exec: context merge Exec->>PM: createInstance(plugin_id_2) PM-->>Exec: Plugin #2 Exec->>P2: execute(updated context) P2-->>Exec: pluginResult + context Exec->>Exec: context merge

これにより、1つの TEMPLATEENTITY の中で

  • 先に値を作る
  • 次に通知する
  • 次に保存する

のような複数段の動作も実現しやすくなっています。


13. GROUP 内のデータ引き継ぎ

あなたが特に重視している点を、そのまま整理すると次の通りです。

13-1. 同一 GROUP_ID 内では引き継がれる

GROUP の中で Template #1 → #2 → #3 と流れるとき、次が引き継がれます。

  • input.raws
  • input.results
  • プラグインが返した context の反映結果

図で見るとこうです。

flowchart LR A[TEMPLATEENTITY #1] -->|updated input.raws| B[TEMPLATEENTITY #2] A -->|updated input.results| B B -->|updated input.raws| C[TEMPLATEENTITY #3] B -->|updated input.results| C

13-2. 別 GROUP_ID には引き継がれない

同じ triggerType で複数 GROUP_ID が起動されても、GROUP ごとに run() が独立しています。 したがって、GROUP A の途中結果が GROUP B に混ざることはありません。

flowchart TD A[trigger = CRON_15M] --> B[GROUP A] A --> C[GROUP B] B --> D[Aのfinal_raws / Aのresults] C --> E[Bのfinal_raws / Bのresults] D -.共有しない.-> E

これは設計上かなり大事で、同一イベントをきっかけに複数の自動化を走らせても相互汚染しないという意味です。


14. プラグイン内 fire のときのデータ引き継ぎ

これも重要です。 プラグインの中で次のトリガを発火する場合、

  • 直前までの context
  • そのときの input.raws
  • 親トレース情報

を使って fire() できます。

例えば Kintone 取得プラグインでは、取得した RAW を context['input']['raws']['kintone'] に積んだあと、それをそのまま fire() の第3引数に渡しています。

つまり概念的にはこうです。

flowchart TD A["親GROUP内で Plugin 実行"] --> B["context.input.raws.kintone に取得結果を格納"] B --> C["KINTONE_RECEIVED を fire"] C --> D["子トリガの GROUP 起動"] D --> E["子GROUPの最初の input.raws に同じデータが入る"]

これは、あなたの言う

プラグイン内でトリガポイントが存在する場合は、直前に持つ各種情報をそのままそのプラグインで発火された次の TEMPLATEENTITY に引き継ぐ

に対応しています。

厳密には「同じ GROUP の次段」ではなく、新しい triggerType の fire として別 GROUP が起動する形ですが、データの受け渡し感覚としてはかなり近いです。


15. 開発者目線での「実際の理解のしかた」

IoT 開発者向けにかなり雑に言い換えると、SWPF AUTOMATION は次のように理解すると分かりやすいです。

15-1. TriggerEntity は割り込みベクタ表のようなもの

  • CRON_15M が来たらどの GROUP を動かすか
  • LINE_RECEIVED が来たらどの GROUP を動かすか
  • KINTONE_RECEIVED が来たらどの GROUP を動かすか

を定義するので、マイコンで言えば イベントディスパッチ表 に近いです。

15-2. GROUP_ID はパイプライン

GROUP は、

  • 入力データを受け取り
  • 各ステップで加工し
  • 中間結果を足しながら
  • 後段に流す

ので、シェルのパイプや、Node-RED の直列フローに近いです。

15-3. PARAMS_JSON は配線変換

センサ側のキー名をそのままロジックに使うと壊れやすいので、 PARAMS_JSON は

  • 実データの場所
  • ロジック側の変数名

を変換する 配線アダプタ と考えると分かりやすいです。

15-4. RULE_JSON は分岐回路

RULE_JSON は

  • 温度が高いなら A
  • 低いなら B
  • さらに score が高いなら C も追加

のような分岐です。 if/else と AND/OR を JSON 化したものと考えればよいです。

15-5. TEMPLATE_JSON は命令本体

最後に実際の「何をやるか」が TEMPLATE_JSON です。

  • メール送信
  • LINE返信
  • ChatGPT 呼び出し
  • Queue投入
  • SwitchBot制御
  • 固定値投入
  • 式評価

がここで決まります。


16. 典型例: CRON → Kintone取得 → KINTONE_RECEIVED → ChatGPT → メール

全体を1本の例で見ると分かりやすいです。

flowchart TD A[Drupal Cron] --> B[CRON_15M 発火] B --> C[TriggerEntity: CRON_15M → GROUP_IMPORT] C --> D[GROUP_IMPORT.Template#1] D --> E[FetchRawDataFromKintone Plugin] E --> F[input.raws.kintone に records 格納] F --> G[fire parent_uid, KINTONE_RECEIVED, fire_raws] G --> H[TriggerEntity: KINTONE_RECEIVED → GROUP_ANALYZE] H --> I[GROUP_ANALYZE.Template#1] I --> J[PARAMS_MAP 作成] J --> K[RULE_JSON で template_id 選択] K --> L[EXPRESSION で score 計算] L --> M[input.results.score 保存] M --> N[次Templateで ChatGPT Plugin] N --> O[結果を input.raws / input.results に反映] O --> P[次Templateで MailSendExecute] P --> Q[メール送信]

この例で押さえるポイントはこうです。

  • 最初の CRON はあくまで起動だけ
  • 外部データ取得はプラグインが担当
  • 取得後に別トリガを fire して次の処理系へ渡せる
  • 中間結果は input.results
  • 生データや下流に渡したいデータは input.raws
  • 最後の通知や保存は COMMAND + plugin 実行

17. input.rawsinput.results の使い分け

ここは実運用で重要なので分けて整理します。

項目主な用途典型例
input.raws外部から来たデータ、または下流へそのまま渡したい構造体MQTT records, Kintone records, weather API応答, plugin取得データ
input.results式計算・固定値投入・中間判定結果score, alertmessage, blogtitle, diagnosis_summary
input.params_map評価・置換のために見やすく再編成した参照表TEMP, HUM, result.score.value

使い分けの感覚としては、

  • raws = 荷物置き場
  • results = 加工結果メモ
  • params_map = 参照しやすい目次

です。


18. 実装上の注意点

18-1. GROUP をまたいで値はつながらない

同じ triggerType で複数 GROUP をぶら下げても、処理は独立です。 そのため、GROUP A の計算結果を GROUP B で使いたいなら、明示的に別の保存先へ書くか、別トリガ fire 時の rawData に入れて渡す必要があります。

18-2. EXPRESSION / LITERAL は plugin を実行しない

これはかなり大事です。 EXPRESSION / LITERAL は データを作るだけ で、通知や保存はしません。 通知や保存したければ、その後段に COMMAND を置く必要があります。

18-3. PARAMSJSON をきちんと作らないと RULEJSON が読みにくくなる

生キーを RULE に直接埋めると、後からデータ元が変わったとき全部壊れます。 この仕組みでは PARAMS_JSON を丁寧に作るほど、ルールとテンプレートが長持ちします。

18-4. plugin 内 fire は便利だが「新しい起動」になる

感覚としては前処理から後処理へ自然につながりますが、内部的には 別トリガの fire です。 したがって、

  • どの uid で fire するか
  • どの triggerType で fire するか
  • rawData に何を積むか

を明確にしないと、意図しないユーザーや GROUP が動く可能性があります。


19. 最終まとめ

SWPF AUTOMATION を一文で言うと、

トリガで GROUP を起動し、GROUP 内の TEMPLATEENTITY を順番に流し、その中で PARAMSMAP → RULEJSON → TEMPLATE_JSON → Plugin/Expression/Literal を通してデータを展開していく仕組み

です。

理解の順番としては、次の順が最も分かりやすいです。

  1. TriggerType はイベント名
  2. TriggerEntity はイベント名と GROUP_ID の対応表
  3. GROUP_ID は TemplateEntity の実行列
  4. TemplateEntity は PARAMSJSON + RULEJSON + TEMPLATE_JSON の1ステップ定義
  5. PARAMSJSON で raw を別名化して PARAMSMAP を作る
  6. RULEJSON で使う TEMPLATEID を決める
  7. TEMPLATE_JSON で COMMAND / EXPRESSION / LITERAL を実行する
  8. 結果は input.raws / input.results に載り、次へ渡る
  9. 同一 GROUP 内では渡るが、別 GROUP には渡らない
  10. plugin 内 fire を使うと、新しい trigger として次の GROUP を起動できる

20. 実コード上の主な確認対象ファイル

今回の説明を追うときは、次のファイルを見ると理解しやすいです。

  • swpfautomation/swpfautomation.module

CRON から fireBatch() へ入る入口

  • swpf_automation/src/Service/SwpfAutomationFireService.php

TriggerEntity を拾って GROUP を起動する本体

  • swpf_automation/src/Service/SwpfAutomationGroupRunner.php

GROUP_ID 配下の TEMPLATEENTITY を順番実行し、raws/results を引き継ぐ層

  • swpf_automation/src/Service/SwpfAutomationExecutor.php

PARAMSMAP 作成、RULEJSON 評価、TEMPLATE_JSON 展開、plugin 実行までの中核

  • swpf_automation/src/Service/SwpfAutomationContextBuilder.php

Template / Profile / DeviceService を集約して bundle を組み立てる層

  • swpf_automation/src/Plugin/SwpfExecute/*.php

実際の COMMAND 実行先


21. 補足

今回確認できた範囲では、PARAMSMAP の生成ロジック本体と RULEJSON の評価ロジック本体は swpf_common サービスに委譲されています。

  • @swpfcommon.paramsmap_builder
  • @swpfcommon.evaljson

そのため、演算子の全仕様や params 展開の厳密な末端実装は COMMON 側を見る必要があります。 ただし、AUTOMATION 側の流れとしては本書の理解でほぼそのまま追えます。


21. 実際のサンプルJSON付き版

ここからは、上の説明を実際に設定する人がイメージしやすいように、 できるだけ現実的な JSON 例を付けて説明します。

注意:

  • 以下の JSON は 理解用のサンプル です
  • 実環境ではフィールド名やラッパ構造が少し異なる可能性があります
  • ただし、考え方・責務分担・データの流れ はこのまま理解して問題ありません

22. サンプル全体シナリオ

まずは全体シナリオを固定します。

22-1. 例のシナリオ

「15分ごとの CRON で Kintone データを取得し、異常判定し、必要なら LINE 通知する」

処理の流れ:

  1. CRON_15M が発火
  2. TriggerEntity が GROUPIMPORTKINTONE を起動
  3. その GROUP 内で Kintone データ取得プラグインを実行
  4. 取得プラグインが KINTONE_RECEIVED を fire
  5. TriggerEntity が GROUPANALYZEKINTONE を起動
  6. PARAMS_JSON でデータを別名化
  7. RULE_JSON で異常かどうか判定
  8. 異常なら EXPRESSION でメッセージを組み立てる
  9. COMMAND で LINE 通知する

図にするとこうです。

flowchart TD A[CRON_15M] --> B[TriggerEntity] B --> C[GROUP_IMPORT_KINTONE] C --> D[Kintone取得 Plugin] D --> E[fire KINTONE_RECEIVED] E --> F[TriggerEntity] F --> G[GROUP_ANALYZE_KINTONE] G --> H[PARAMS_MAP作成] H --> I[RULE_JSON判定] I --> J[EXPRESSIONで通知文作成] J --> K[LINE Pluginで通知]

23. Trigger 定数のイメージ

まず、起動ポイントはあらかじめ CONST 的に定義されている前提です。

23-1. トリガ定数のイメージ

const CRON_1M = 'CRON_1M';
const CRON_5M = 'CRON_5M';
const CRON_15M = 'CRON_15M';
const LINE_RECEIVED = 'LINE_RECEIVED';
const MQTT_RECEIVED = 'MQTT_RECEIVED';
const KINTONE_RECEIVED = 'KINTONE_RECEIVED';
const SWITCHBOT_RECEIVED = 'SWITCHBOT_RECEIVED';

これは「処理そのもの」ではなく、イベント名の定義です。

マイコンで言えば、

  • タイマ割り込み
  • GPIO 割り込み
  • UART受信完了

のような入口ラベルに近いです。


24. TriggerEntity のサンプル

TriggerEntity は「どのトリガで、どの GROUP_ID を動かすか」の設定です。

24-1. CRON 用 TriggerEntity の例

{
  "id": 101,
  "uid": 1,
  "enabled": 1,
  "trigger_type": "CRON_15M",
  "template_group_id": "GROUP_IMPORT_KINTONE",
  "weight": 10,
  "title": "15分ごとにKintone取得開始"
}

意味:

  • ユーザー uid=1
  • CRON_15M が来たら
  • GROUPIMPORTKINTONE を起動する

24-2. KINTONE_RECEIVED 用 TriggerEntity の例

{
  "id": 102,
  "uid": 1,
  "enabled": 1,
  "trigger_type": "KINTONE_RECEIVED",
  "template_group_id": "GROUP_ANALYZE_KINTONE",
  "weight": 10,
  "title": "Kintone受信後に解析と通知"
}

意味:

  • 同じユーザー uid=1
  • KINTONE_RECEIVED が fire されたら
  • GROUPANALYZEKINTONE を起動する

24-3. 同じトリガに複数 GROUP をぶら下げる例

[
  {
    "id": 201,
    "uid": 1,
    "enabled": 1,
    "trigger_type": "CRON_15M",
    "template_group_id": "GROUP_IMPORT_KINTONE",
    "weight": 10
  },
  {
    "id": 202,
    "uid": 1,
    "enabled": 1,
    "trigger_type": "CRON_15M",
    "template_group_id": "GROUP_IMPORT_WEATHER",
    "weight": 20
  }
]

この場合、同じ CRON_15M

  • GROUPIMPORTKINTONE
  • GROUPIMPORTWEATHER

の両方が動きます。

ただし前述の通り、GROUP が違うのでデータは共有されません。


25. GROUP_ID にぶら下がる TEMPLATEENTITY のサンプル

次に、GROUP に属する実行ステップの例です。

25-1. GROUPIMPORTKINTONE の例

[
  {
    "id": 301,
    "uid": 1,
    "group_id": "GROUP_IMPORT_KINTONE",
    "weight": 10,
    "enabled": 1,
    "title": "Kintoneからデータ取得",
    "plugin_id": "fetch_raw_data_from_kintone"
  }
]

この GROUP は 1ステップだけです。

25-2. GROUPANALYZEKINTONE の例

[
  {
    "id": 401,
    "uid": 1,
    "group_id": "GROUP_ANALYZE_KINTONE",
    "weight": 10,
    "enabled": 1,
    "title": "取得データを解析してスコア計算"
  },
  {
    "id": 402,
    "uid": 1,
    "group_id": "GROUP_ANALYZE_KINTONE",
    "weight": 20,
    "enabled": 1,
    "title": "必要なら通知"
  }
]

ここで重要なのは、

  • weight=10 が先
  • weight=20 が後

であり、401 の結果を 402 が使えることです。


26. Kintone取得プラグインの入出力イメージ

ここでは最初の GROUP で動くプラグインをイメージ化します。

26-1. プラグイン実行前の context

{
  "uid": 1,
  "trigger_type": "CRON_15M",
  "input": {
    "raws": {},
    "results": {}
  },
  "meta": {
    "source": "cron"
  }
}

26-2. プラグインが Kintone から取得した生データ例

{
  "records": [
    {
      "record_id": "5001",
      "device_id": "esp32_C80CABF16E20",
      "temp": 31.2,
      "hum": 68.5,
      "co2": 1024,
      "captured_at": "2026-04-19 15:00:00"
    },
    {
      "record_id": "5002",
      "device_id": "esp32_C80CABF16E20",
      "temp": 32.8,
      "hum": 70.1,
      "co2": 1180,
      "captured_at": "2026-04-19 15:15:00"
    }
  ]
}

26-3. プラグイン実行後の context 例

{
  "uid": 1,
  "trigger_type": "CRON_15M",
  "input": {
    "raws": {
      "kintone": {
        "records": [
          {
            "record_id": "5001",
            "device_id": "esp32_C80CABF16E20",
            "temp": 31.2,
            "hum": 68.5,
            "co2": 1024,
            "captured_at": "2026-04-19 15:00:00"
          },
          {
            "record_id": "5002",
            "device_id": "esp32_C80CABF16E20",
            "temp": 32.8,
            "hum": 70.1,
            "co2": 1180,
            "captured_at": "2026-04-19 15:15:00"
          }
        ]
      }
    },
    "results": {}
  },
  "meta": {
    "fetched_count": 2,
    "source": "cron"
  }
}

26-4. このあと fire されるデータの例

{
  "kintone": {
    "records": [
      {
        "record_id": "5001",
        "device_id": "esp32_C80CABF16E20",
        "temp": 31.2,
        "hum": 68.5,
        "co2": 1024,
        "captured_at": "2026-04-19 15:00:00"
      },
      {
        "record_id": "5002",
        "device_id": "esp32_C80CABF16E20",
        "temp": 32.8,
        "hum": 70.1,
        "co2": 1180,
        "captured_at": "2026-04-19 15:15:00"
      }
    ]
  }
}

つまり、プラグイン内 fire は概念的にはこうです。

$fire_raws = $context['input']['raws'];
$fireService->fire($uid, 'KINTONE_RECEIVED', $fire_raws, $options);

このため、取得済みデータをそのまま次の自動化へ渡せるわけです。


27. 解析用 TEMPLATEENTITY の実サンプル

ここからは GROUPANALYZEKINTONE の中身を具体化します。

まず 1件目の TEMPLATEENTITY です。

27-1. TEMPLATEENTITY #401 のサンプル

{
  "id": 401,
  "uid": 1,
  "group_id": "GROUP_ANALYZE_KINTONE",
  "weight": 10,
  "enabled": 1,
  "title": "温度とCO2から警告判定",
  "params_json": {
    "TEMP": "kintone.records.1.temp",
    "HUM": "kintone.records.1.hum",
    "CO2": "kintone.records.1.co2",
    "DEVICE_ID": "kintone.records.1.device_id",
    "CAPTURED_AT": "kintone.records.1.captured_at"
  },
  "rule_json": {
    "if": {
      "op": "OR",
      "conditions": [
        {
          "left": "TEMP",
          "operator": ">=",
          "right": 32.0
        },
        {
          "left": "CO2",
          "operator": ">=",
          "right": 1100
        }
      ]
    },
    "then": ["CALC_ALERT_LEVEL", "BUILD_ALERT_MESSAGE"],
    "else": ["CALC_NORMAL_LEVEL"]
  },
  "template_json": {
    "CALC_ALERT_LEVEL": {
      "type": "LITERAL",
      "result_key": "alert_level",
      "value": "warning"
    },
    "BUILD_ALERT_MESSAGE": {
      "type": "EXPRESSION",
      "result_key": "alert_message",
      "expr": "'Device=' ~ {DEVICE_ID} ~ ' temp=' ~ {TEMP} ~ ' hum=' ~ {HUM} ~ ' co2=' ~ {CO2}"
    },
    "CALC_NORMAL_LEVEL": {
      "type": "LITERAL",
      "result_key": "alert_level",
      "value": "normal"
    }
  }
}

この 1件でやっていることは:

  • params_json で Kintone データを別名にする
  • rule_json で異常かどうか判定する
  • 異常なら alert_level=warning を入れる
  • 異常なら通知文 alert_message も作る
  • 正常なら alert_level=normal を入れる

28. PARAMS_JSON のサンプルを分解して理解する

28-1. 例

{
  "TEMP": "kintone.records.1.temp",
  "HUM": "kintone.records.1.hum",
  "CO2": "kintone.records.1.co2",
  "DEVICE_ID": "kintone.records.1.device_id",
  "CAPTURED_AT": "kintone.records.1.captured_at"
}

28-2. 意味

これは、RULEJSON や TEMPLATEJSON の中では

  • TEMP
  • HUM
  • CO2
  • DEVICE_ID
  • CAPTURED_AT

という短い名前で使いたい、という宣言です。

もとのデータ構造が深い場合でも、ロジック側を簡潔にできます。

28-3. IoT 開発者向けの感覚

これは、マイコン側でいうと

float t = sensorData.moduleA.temp;
float h = sensorData.moduleA.hum;

のように、ローカル変数へ取り出してから計算する感覚に近いです。

サーバー側ではその「取り出し定義」を JSON 化している、と考えると分かりやすいです。


29. PARAMS_MAP が実際にどう生成されるかの例

上の paramsjsoninput.raws から、評価用の paramsmap が作られます。

29-1. 元データ

{
  "input": {
    "raws": {
      "kintone": {
        "records": [
          {
            "record_id": "5001",
            "device_id": "esp32_C80CABF16E20",
            "temp": 31.2,
            "hum": 68.5,
            "co2": 1024,
            "captured_at": "2026-04-19 15:00:00"
          },
          {
            "record_id": "5002",
            "device_id": "esp32_C80CABF16E20",
            "temp": 32.8,
            "hum": 70.1,
            "co2": 1180,
            "captured_at": "2026-04-19 15:15:00"
          }
        ]
      }
    },
    "results": {}
  }
}

29-2. params_map の生成例

{
  "TEMP": 32.8,
  "HUM": 70.1,
  "CO2": 1180,
  "DEVICE_ID": "esp32_C80CABF16E20",
  "CAPTURED_AT": "2026-04-19 15:15:00"
}

この時点で RULE_JSON からは、深いパスを意識せずに

  • TEMP
  • CO2

で判定できるようになります。


30. RULE_JSON のサンプルを詳しく読む

30-1. 単純な OR 判定例

{
  "if": {
    "op": "OR",
    "conditions": [
      {
        "left": "TEMP",
        "operator": ">=",
        "right": 32.0
      },
      {
        "left": "CO2",
        "operator": ">=",
        "right": 1100
      }
    ]
  },
  "then": ["CALC_ALERT_LEVEL", "BUILD_ALERT_MESSAGE"],
  "else": ["CALC_NORMAL_LEVEL"]
}

意味:

  • TEMP >= 32.0 または CO2 >= 1100 なら異常扱い
  • 異常なら CALCALERTLEVELBUILDALERTMESSAGE を実行
  • それ以外は CALCNORMALLEVEL を実行

30-2. もっと複雑な AND / OR ネスト例

{
  "if": {
    "op": "OR",
    "conditions": [
      {
        "op": "AND",
        "conditions": [
          {
            "left": "TEMP",
            "operator": ">=",
            "right": 30.0
          },
          {
            "left": "HUM",
            "operator": ">=",
            "right": 75.0
          }
        ]
      },
      {
        "left": "CO2",
        "operator": ">=",
        "right": 1200
      }
    ]
  },
  "then": ["CALC_ALERT_LEVEL", "BUILD_ALERT_MESSAGE"],
  "else": ["CALC_NORMAL_LEVEL"]
}

意味:

  • (TEMP >= 30.0 AND HUM >= 75.0)
  • または CO2 >= 1200
  • なら異常

これは実際の if 文にすると、だいたい次の感覚です。

if ( (
      $TEMP >= 30.0 && $HUM >= 75.0
     ) || $CO2 >= 1200 ) {
  // alert
}

31. TEMPLATE_JSON のサンプルを詳しく読む

31-1. LITERAL の例

{
  "type": "LITERAL",
  "result_key": "alert_level",
  "value": "warning"
}

意味:

  • input.results.alert_level
  • 固定値 warning を入れる

実行後イメージ:

{
  "input": {
    "results": {
      "alert_level": {
        "key": "alert_level",
        "value": "warning",
        "type": "LITERAL",
        "template_id": "CALC_ALERT_LEVEL"
      }
    }
  }
}

31-2. EXPRESSION の例

{
  "type": "EXPRESSION",
  "result_key": "alert_message",
  "expr": "'Device=' ~ {DEVICE_ID} ~ ' temp=' ~ {TEMP} ~ ' hum=' ~ {HUM} ~ ' co2=' ~ {CO2}"
}

意味:

  • DEVICE_ID, TEMP, HUM, CO2 を使って文字列を組み立てる
  • 結果を input.results.alert_message に入れる

実行後イメージ:

{
  "input": {
    "results": {
      "alert_message": {
        "key": "alert_message",
        "value": "Device=esp32_C80CABF16E20 temp=32.8 hum=70.1 co2=1180",
        "type": "EXPRESSION",
        "template_id": "BUILD_ALERT_MESSAGE"
      }
    }
  }
}

31-3. COMMAND の例

次の TEMPLATEENTITY では、作った値を使って通知することができます。

{
  "SEND_LINE_ALERT": {
    "type": "COMMAND",
    "plugin": "line_message_execute",
    "command": {
      "REPLYHEADER": "自動診断結果",
      "REPLY": "{result.alert_message.value}",
      "LABEL": "警告",
      "LEVEL": "{result.alert_level.value}"
    }
  }
}

意味:

  • LINE 通知プラグインを呼ぶ
  • result.alert_message.value を本文に差し込む
  • result.alert_level.value をプラグインへ渡す

32. 結果が params_map に再反映される例

ここが非常に重要です。

先ほどの LITERAL / EXPRESSION 実行後、input.results に入った値は後段のテンプレートから参照できるように、再度 params_map に載せ直されます。

32-1. input.results の例

{
  "alert_level": {
    "key": "alert_level",
    "value": "warning",
    "type": "LITERAL",
    "template_id": "CALC_ALERT_LEVEL"
  },
  "alert_message": {
    "key": "alert_message",
    "value": "Device=esp32_C80CABF16E20 temp=32.8 hum=70.1 co2=1180",
    "type": "EXPRESSION",
    "template_id": "BUILD_ALERT_MESSAGE"
  }
}

32-2. 再構築後の params_map 例

{
  "TEMP": 32.8,
  "HUM": 70.1,
  "CO2": 1180,
  "DEVICE_ID": "esp32_C80CABF16E20",
  "CAPTURED_AT": "2026-04-19 15:15:00",
  "result.alert_level.value": "warning",
  "result.alert_message.value": "Device=esp32_C80CABF16E20 temp=32.8 hum=70.1 co2=1180"
}

このため後段では

  • {result.alert_level.value}
  • {result.alert_message.value}

のように参照できます。


33. 通知用 TEMPLATEENTITY のサンプル

次は 2件目の TEMPLATEENTITY です。

33-1. TEMPLATEENTITY #402 のサンプル

{
  "id": 402,
  "uid": 1,
  "group_id": "GROUP_ANALYZE_KINTONE",
  "weight": 20,
  "enabled": 1,
  "title": "警告時だけLINE通知",
  "params_json": {
    "ALERT_LEVEL": "result.alert_level.value",
    "ALERT_MESSAGE": "result.alert_message.value"
  },
  "rule_json": {
    "if": {
      "left": "ALERT_LEVEL",
      "operator": "==",
      "right": "warning"
    },
    "then": ["SEND_LINE_ALERT"],
    "else": []
  },
  "template_json": {
    "SEND_LINE_ALERT": {
      "type": "COMMAND",
      "plugin": "line_message_execute",
      "command": {
        "REPLYHEADER": "自動診断結果",
        "REPLY": "{ALERT_MESSAGE}",
        "LABEL": "警告",
        "LEVEL": "{ALERT_LEVEL}"
      }
    }
  }
}

この TEMPLATEENTITY は、前段 #401 が作った結果だけを見ています。

つまり、同じ GROUP 内では次のように流れます。

flowchart LR A[#401 解析テンプレート] -->|input.results.alert_level| B[#402 通知テンプレート] A -->|input.results.alert_message| B

34. 実行前から実行後までの context 全体例

ここでは、KINTONERECEIVED で GROUPANALYZE_KINTONE が始まってから、通知まで進んだ場合の流れをまとめます。

34-1. GROUP 開始時の context

{
  "uid": 1,
  "trigger_type": "KINTONE_RECEIVED",
  "input": {
    "raws": {
      "kintone": {
        "records": [
          {
            "record_id": "5001",
            "device_id": "esp32_C80CABF16E20",
            "temp": 31.2,
            "hum": 68.5,
            "co2": 1024,
            "captured_at": "2026-04-19 15:00:00"
          },
          {
            "record_id": "5002",
            "device_id": "esp32_C80CABF16E20",
            "temp": 32.8,
            "hum": 70.1,
            "co2": 1180,
            "captured_at": "2026-04-19 15:15:00"
          }
        ]
      }
    },
    "results": {}
  },
  "meta": {
    "parent_trigger": "CRON_15M"
  }
}

34-2. #401 実行後の context

{
  "uid": 1,
  "trigger_type": "KINTONE_RECEIVED",
  "input": {
    "raws": {
      "kintone": {
        "records": [
          {
            "record_id": "5001",
            "device_id": "esp32_C80CABF16E20",
            "temp": 31.2,
            "hum": 68.5,
            "co2": 1024,
            "captured_at": "2026-04-19 15:00:00"
          },
          {
            "record_id": "5002",
            "device_id": "esp32_C80CABF16E20",
            "temp": 32.8,
            "hum": 70.1,
            "co2": 1180,
            "captured_at": "2026-04-19 15:15:00"
          }
        ]
      }
    },
    "results": {
      "alert_level": {
        "key": "alert_level",
        "value": "warning",
        "type": "LITERAL",
        "template_id": "CALC_ALERT_LEVEL"
      },
      "alert_message": {
        "key": "alert_message",
        "value": "Device=esp32_C80CABF16E20 temp=32.8 hum=70.1 co2=1180",
        "type": "EXPRESSION",
        "template_id": "BUILD_ALERT_MESSAGE"
      }
    },
    "params_map": {
      "TEMP": 32.8,
      "HUM": 70.1,
      "CO2": 1180,
      "DEVICE_ID": "esp32_C80CABF16E20",
      "CAPTURED_AT": "2026-04-19 15:15:00",
      "result.alert_level.value": "warning",
      "result.alert_message.value": "Device=esp32_C80CABF16E20 temp=32.8 hum=70.1 co2=1180"
    }
  }
}

34-3. #402 実行時にプラグインへ渡る command 例

{
  "REPLYHEADER": "自動診断結果",
  "REPLY": "Device=esp32_C80CABF16E20 temp=32.8 hum=70.1 co2=1180",
  "LABEL": "警告",
  "LEVEL": "warning"
}

35. MQTT 受信のサンプルJSON版

次は、IoT 開発者向けにより身近な MQTT を例にします。

35-1. ESP32 から来る payload 例

{
  "schema": "swpf.env.v1",
  "type": "env",
  "device_id": "esp32_C80CABF16E20",
  "ts": 1776142631,
  "temperature": 26.5,
  "humidity": 47.05,
  "lux": 237.49,
  "pressure_hpa": 1013.8,
  "co2": 812,
  "soil_moisture": 41,
  "wifi_rssi": -51,
  "free_heap": 208740,
  "min_free_heap": 199984,
  "uptime_sec": 12
}

35-2. これを受けた最初の rawData イメージ

{
  "mqtt": {
    "topic": "swpf/user001/esp32_C80CABF16E20/env",
    "payload": {
      "schema": "swpf.env.v1",
      "type": "env",
      "device_id": "esp32_C80CABF16E20",
      "ts": 1776142631,
      "temperature": 26.5,
      "humidity": 47.05,
      "lux": 237.49,
      "pressure_hpa": 1013.8,
      "co2": 812,
      "soil_moisture": 41,
      "wifi_rssi": -51,
      "free_heap": 208740,
      "min_free_heap": 199984,
      "uptime_sec": 12
    }
  }
}

35-3. これに対する PARAMS_JSON 例

{
  "TEMP": "mqtt.payload.temperature",
  "HUM": "mqtt.payload.humidity",
  "CO2": "mqtt.payload.co2",
  "SOIL": "mqtt.payload.soil_moisture",
  "RSSI": "mqtt.payload.wifi_rssi",
  "FREE_HEAP": "mqtt.payload.free_heap",
  "MIN_HEAP": "mqtt.payload.min_free_heap",
  "DEVICE_ID": "mqtt.payload.device_id"
}

35-4. ルール例

{
  "if": {
    "op": "OR",
    "conditions": [
      {
        "left": "SOIL",
        "operator": "<=",
        "right": 20
      },
      {
        "left": "FREE_HEAP",
        "operator": "<=",
        "right": 120000
      }
    ]
  },
  "then": ["BUILD_MQTT_ALERT", "SEND_LINE_ALERT"],
  "else": []
}

意味:

  • 土壌水分が低すぎる
  • またはヒープ残量が少なすぎる

なら警告する。

これは IoT 運用ではかなり実用的です。


36. LITERAL / EXPRESSION / COMMAND を一列で使うサンプル

ここでは、1つの TEMPLATEENTITY 内で 3種類を混ぜるイメージを示します。

36-1. 例

{
  "params_json": {
    "TEMP": "mqtt.payload.temperature",
    "HUM": "mqtt.payload.humidity"
  },
  "rule_json": {
    "if": {
      "left": "TEMP",
      "operator": ">=",
      "right": 30
    },
    "then": ["SET_STATUS", "MAKE_MESSAGE", "SEND_LINE"],
    "else": []
  },
  "template_json": {
    "SET_STATUS": {
      "type": "LITERAL",
      "result_key": "status",
      "value": "high_temp"
    },
    "MAKE_MESSAGE": {
      "type": "EXPRESSION",
      "result_key": "message",
      "expr": "'温度=' ~ {TEMP} ~ ' 湿度=' ~ {HUM}"
    },
    "SEND_LINE": {
      "type": "COMMAND",
      "plugin": "line_message_execute",
      "command": {
        "REPLYHEADER": "センサー通知",
        "REPLY": "{result.message.value}"
      }
    }
  }
}

36-2. 順番の意味

  1. SET_STATUS で固定値を結果へ入れる
  2. MAKE_MESSAGE で文字列を組み立てる
  3. SEND_LINE でそれを通知する

この並びは、PLC や Node-RED のフロー感覚で理解すると分かりやすいです。


37. GROUP をまたぐとデータが消える例 / 消えない例

ここは誤解しやすいので、サンプルで明確にします。

37-1. 同じ GROUP 内なら使える例

GROUPANALYZEKINTONE の中:

  • #401 が result.alert_message.value を作る
  • #402 がそれを使って LINE 送信する

これは OK です。

37-2. 別 GROUP ではそのまま使えない例

CRON_15M に対して次の2つが直接ぶら下がっているとします。

[
  {
    "trigger_type": "CRON_15M",
    "template_group_id": "GROUP_A"
  },
  {
    "trigger_type": "CRON_15M",
    "template_group_id": "GROUP_B"
  }
]

この場合:

  • GROUP_Aresult.score.value=80 を作っても
  • GROUP_B ではその値は見えません

図にするとこうです。

flowchart LR T[CRON_15M] --> A[GROUP_A] T --> B[GROUP_B] A --> A1[result.score.value = 80] B --> B1[GROUP_Aのscoreは見えない]

37-3. 別 GROUP へ渡したい場合の例

その場合は、GROUP_A のプラグイン内で次のように fire 時 rawData に積む必要があります。

{
  "from_group_a": {
    "score": 80,
    "message": "warning"
  }
}

つまり、別 GROUP に渡したい値は意識的に載せ替える必要があります。


38. プラグイン内 fire の具体例

38-1. 親プラグイン内でのイメージ

$context['input']['raws']['imported'] = [
  'score' => 80,
  'message' => 'warning',
  'source' => 'kintone',
];

$fire_raws = $context['input']['raws'];
$fireService->fire($uid, 'ANALYZE_IMPORTED_DATA', $fire_raws, [
  'parent_trigger' => 'KINTONE_RECEIVED',
]);

38-2. 子トリガ側で受ける rawData の例

{
  "imported": {
    "score": 80,
    "message": "warning",
    "source": "kintone"
  }
}

38-3. 子 GROUP 側 PARAMS_JSON の例

{
  "SCORE": "imported.score",
  "MESSAGE": "imported.message",
  "SOURCE": "imported.source"
}

このようにすると、子 GROUP 側で

  • SCORE
  • MESSAGE
  • SOURCE

として綺麗に参照できます。


39. 実際の開発でよくある構成パターン

ここでは、実用でよくあるパターンを JSON イメージ付きで整理します。

39-1. パターンA: 取得 GROUP と解析 GROUP を分ける

構成:

  • CRON15MGROUPIMPORT
  • GROUP_IMPORT の plugin が外部データ取得
  • plugin 内 fire('DATA_IMPORTED')
  • DATAIMPORTEDGROUPANALYZE

利点:

  • 取得処理と解析処理を分離できる
  • 解析 GROUP を他トリガからも再利用しやすい

39-2. パターンB: 1 GROUP に全部入れる

構成:

  • CRON15MGROUPONEFLOW
  • Step1: 取得
  • Step2: 判定
  • Step3: 通知

利点:

  • 設定数が少ない
  • データ引き継ぎが分かりやすい

欠点:

  • GROUP が巨大化しやすい
  • 再利用しにくい

39-3. パターンC: デバイス別・通知先別に GROUP を分ける

構成例:

[
  {
    "trigger_type": "MQTT_RECEIVED",
    "template_group_id": "GROUP_SENSOR_SAVE"
  },
  {
    "trigger_type": "MQTT_RECEIVED",
    "template_group_id": "GROUP_SENSOR_ALERT"
  }
]

意味:

  • 受信したら保存系 GROUP を動かす
  • 同時に通知系 GROUP も動かす

ただし繰り返しですが、この2つは独立実行です。


40. サンプル: メール送信版 TEMPLATE_JSON

LINE だけでなくメールも同じ考え方で動きます。

40-1. COMMAND 例

{
  "SEND_MAIL_ALERT": {
    "type": "COMMAND",
    "plugin": "mail_send_execute",
    "command": {
      "subject": "[警告] {DEVICE_ID} のセンサー異常",
      "body": "時刻={CAPTURED_AT}\n{result.alert_message.value}",
      "to": "admin@example.com"
    }
  }
}

このように、COMMAND は

  • プラグイン種類
  • そのプラグインに渡すデータ

を定義する層です。

つまり、TEMPLATE_JSON は「関数呼び出しの引数定義」にかなり近いです。


41. サンプル: ChatGPT 呼び出し版 TEMPLATE_JSON

AI 系の実行も、考え方は同じです。

41-1. 例

{
  "SEND_TO_GPT": {
    "type": "COMMAND",
    "plugin": "chatgpt_execute",
    "command": {
      "PROMPT": "次の環境データを解析してください。TEMP={TEMP}, HUM={HUM}, CO2={CO2}",
      "SYSTEM": "あなたは栽培診断アシスタントです",
      "RESULT_KEY": "gpt_diagnosis"
    }
  }
}

このプラグインが結果を

{
  "input": {
    "raws": {
      "chatgpt": {
        "diagnosis": "高温とCO2上昇が見られます。換気を推奨します。"
      }
    },
    "results": {
      "gpt_diagnosis": {
        "key": "gpt_diagnosis",
        "value": "高温とCO2上昇が見られます。換気を推奨します。",
        "type": "COMMAND_RESULT"
      }
    }
  }
}

のように返せば、後段のテンプレートからさらに利用できます。


42. 実際に設計するときのおすすめ分割

JSON 例を見たうえで、開発者向けにおすすめの分割を書くとこうです。

42-1. まず TriggerEntity を設計する

最初に決めるもの:

  • どのイベントで起動するか
  • そのイベントで何グループ動かすか

42-2. 次に GROUP の責務を決める

GROUP は 1つに何でも詰め込まず、たとえば次の責務に分けると分かりやすいです。

  • IMPORT_GROUP: 外部取得
  • ANALYZE_GROUP: 判定・計算
  • NOTIFY_GROUP: 通知
  • SAVE_GROUP: 保存

42-3. TEMPLATEENTITY は「1ステップ1責務」にする

たとえば:

  • Step1: 必要な値を別名化して判定
  • Step2: 中間メッセージを作成
  • Step3: 通知

のようにすると、デバッグしやすいです。


43. デバッグ時に JSON をどう見るとよいか

実際にハマりやすい順で確認ポイントを書きます。

43-1. まず input.raws を見る

期待した元データが本当に入っているか確認します。

例:

{
  "mqtt": {
    "payload": {
      "temperature": 26.5
    }
  }
}

ここが違っていたら、以降全部ずれます。

43-2. 次に params_map を見る

PARAMS_JSON が正しく効いていれば、ここで短い名前に変換されています。

例:

{
  "TEMP": 26.5
}

もし無ければ:

  • PARAMS_JSON のパス誤り
  • rawData の構造認識違い

が疑わしいです。

43-3. 次に input.results を見る

EXPRESSION / LITERAL の結果がここに出ます。

例:

{
  "status": {
    "value": "warning"
  }
}

43-4. 最後に COMMAND へ渡る値を見る

通知文や保存値が展開後にどうなっているかを確認します。

例:

{
  "REPLY": "Device=esp32_C80CABF16E20 temp=32.8 hum=70.1 co2=1180"
}

ここまで合っていれば、あとはプラグイン内部の問題に絞れます。


44. よくあるミスと JSON 上の見え方

44-1. PARAMS_JSON のキー先を間違える

誤り例:

{
  "TEMP": "mqtt.temperature"
}

実データが mqtt.payload.temperature なら、これは解決できません。

結果:

{
  "TEMP": null
}

または TEMP 自体が生成されないことがあります。

44-2. result の参照先を間違える

誤り例:

{
  "ALERT_MESSAGE": "result.alert_message"
}

実際には result.alert_message.value であるなら、値ではなく構造体を見てしまう、または空になります。

44-3. GROUP をまたいで値が見えると思い込む

これはかなり多いです。

  • GROUP_A で作った result.score.value
  • GROUP_B でそのまま参照

は基本的にできません。

必要なら:

  • 保存する
  • fire 時 rawData に積み替える

のどちらかが必要です。


45. 開発者向けの最終理解モデル

サンプルJSONまで含めて、最終的には次のように理解すると設計しやすいです。

45-1. TriggerEntity はイベントルーティング

trigger_type -> group_id

45-2. GROUP はパイプライン

Template #1 -> Template #2 -> Template #3

45-3. PARAMS_JSON は変数展開のための別名表

実データパス -> ルールで使う短い名前

45-4. RULE_JSON は分岐器

条件成立ならどの template_id を実行するか

45-5. TEMPLATE_JSON は命令辞書

template_id -> 実際の処理内容

45-6. input.raws / results / params_map は役割が違う

raws      = 元データ・構造体
results   = 中間生成物
params_map = 評価/置換のための見やすい参照表

46. 付録: 最小サンプル一式

最後に、最小構成のサンプルをひとまとめに置きます。

46-1. TriggerEntity

{
  "uid": 1,
  "enabled": 1,
  "trigger_type": "MQTT_RECEIVED",
  "template_group_id": "GROUP_SENSOR_ALERT",
  "weight": 10
}

46-2. TemplateEntity

{
  "uid": 1,
  "group_id": "GROUP_SENSOR_ALERT",
  "weight": 10,
  "enabled": 1,
  "params_json": {
    "TEMP": "mqtt.payload.temperature",
    "CO2": "mqtt.payload.co2",
    "DEVICE_ID": "mqtt.payload.device_id"
  },
  "rule_json": {
    "if": {
      "op": "OR",
      "conditions": [
        {
          "left": "TEMP",
          "operator": ">=",
          "right": 30
        },
        {
          "left": "CO2",
          "operator": ">=",
          "right": 1000
        }
      ]
    },
    "then": ["BUILD_MSG", "SEND_LINE"],
    "else": []
  },
  "template_json": {
    "BUILD_MSG": {
      "type": "EXPRESSION",
      "result_key": "message",
      "expr": "'device=' ~ {DEVICE_ID} ~ ' temp=' ~ {TEMP} ~ ' co2=' ~ {CO2}"
    },
    "SEND_LINE": {
      "type": "COMMAND",
      "plugin": "line_message_execute",
      "command": {
        "REPLYHEADER": "MQTT警告",
        "REPLY": "{result.message.value}"
      }
    }
  }
}

46-3. 受信 payload

{
  "mqtt": {
    "payload": {
      "device_id": "esp32_C80CABF16E20",
      "temperature": 31.4,
      "co2": 1050
    }
  }
}

46-4. 実行後の主な結果

{
  "input": {
    "results": {
      "message": {
        "value": "device=esp32_C80CABF16E20 temp=31.4 co2=1050"
      }
    }
  }
}

これだけでも、SWPF AUTOMATION の本質である

  • イベントで起動
  • GROUP を選択
  • PARAMS_MAP を作成
  • RULE で分岐
  • TEMPLATE で実処理
  • 結果を次へ渡す

という流れが成立しています。


47. 補足まとめ

今回追加したサンプルJSON付き版で見てほしいポイントは次の通りです。

  1. TriggerEntity は event → group の対応表
  2. TemplateEntity は 1ステップの設定本体
  3. PARAMS_JSON は rawData から使いやすい名前へ引き直す定義
  4. RULEJSON は templateid を選ぶ分岐
  5. TEMPLATE_JSON は COMMAND / EXPRESSION / LITERAL の命令辞書
  6. 同一 GROUP 内だけ results を自然に引き継げる
  7. 別 GROUP に渡すなら fire 時 rawData に載せる必要がある

 

当サイトまたはIoTカスタムモジュール、開発支援に関するお問い合わせはこちらのメールフォームからお気軽にお問い合わせください。