ケーススタディ:円形ジオフェンスアラーム

概要

このセクションでは、より複雑なルールを作成する方法の詳細な例を紹介します。 このガイドの他のセクションで解説している複数の機能を使用します。

Apama EPLを始めたばかりの場合は、例題をご覧ください。

前提条件

ゴール

位置情報イベントを継続的に送信するトラッキングデバイスに、ジオフェンスの外に移動した場合に自動的にアラームを生成させたいとします。このジオフェンスは円形であり、デバイスごとに個別に構成できるようにする必要があります。アラームは、デバイスがジオフェンスの外側に移動したときに作成されます。外を移動している間は、最初のアラームが有効のままになるため、新しいアラームを作成してはいけません。デバイスがジオフェンスに戻るとすぐに、アラームがクリアされます。

Things Cloudデータモデル

位置イベントの構造 (今回の例で必要な要素):

{
  "id": "...",
  "source": {"id": "..."},
  "text": "...",
  "time": "...",
  "type": "...",
  "c8y_Position": {"alt": ..., "lng": ..., "lat": ...}
}

ジオフェンス設定をデバイスに保存します(半径はメートル単位で設定されます):

{
  "c8y_Geofence": {"lat": ..., "lng": ..., "radius": ...}
}

さらに、設定を完全に削除せずに、各デバイスのジオフェンスアラームを有効/無効にしたいので、 デバイスのc8y_SupportedOperationsに「c8y_Geofence」を追加/削除します:

{
  "c8y_SupportedOperations": [..., "c8y_Geofence", ...]
}

計算

現在の位置と中心との間の距離が設定した半径よりも大きい場合、デバイスはジオフェンスの外側にあります。 必要なのは、2つの地理座標の差を計算できる関数です:

action distance(float lat1, float lon1, float lat2, float lon2) returns float {
	float R := 6371000.0;
	float toRad := float.PI / 180.0;
	float lat1Rad := lat1 * toRad;
	float lat2Rad := lat2 * toRad;
	float deltaLatRad := (lat2-lat1) * toRad;
	float deltaLonRad := (lat2-lat1) * toRad;
	float a := (deltaLatRad/2.0).sin().pow(2.0) * lat1Rad.cos() * lat2Rad.cos() * (deltaLonRad/2.0).sin().pow(2.0);
	float c := 2.0 * a.sqrt().atan2((1.0-a).sqrt());
	return R * c;
}

上記のアクションは距離をメートル単位で返します。

ステップ 1: 入力の絞り込み

このモジュールの主な入力はイベントです。 一致しないイベントをできるだけ早く破棄するには、リスナーでの最初の確認として次を実行します:

monitor.subscribe(Measurement.SUBSCRIBE_CHANNEL);
on all Event() as e {
	if e.params.hasKey("c8y_Position") {
		// we have an event
	}
}

ステップ 2: 必要なデータの収集

次のステップでは、計算用のジオフェンスの設定を取得します。

monitor.subscribe(FindManagedObjectResponse.SUBSCRIBE_CHANNEL);
...
integer reqId := integer.getUnique();
send FindManagedObject(reqId, e.source, new dictionary<string,string>) to FindManagedObject.SEND_CHANNEL;
on FindManagedObjectResponse(reqId = reqId) as resp
   and not FindManagedObjectResponseAck(reqId) {
	  ManagedObject dev := resp.managedObject;
   }

ステップ 3: デバイスがc8y_Geofenceに対応しているか確認

デバイスが使用可能になったら、デバイスにジオフェンスが設定されてあるかどうか、および有効になっているかどうかを確認します(supportedOperationsに「c8y_Geofence」が含まれているか)。c8y_SupportedOperationsの配列を確認するには、indexOf()関数を使用します。この関数は、すべての要素をループし、そのエントリのインデックス、または値が存在しない場合は負の数を返します。ジオフェンス設定については、デバイスに「c8y_Geofence」というフラグメントが含まれているかどうかの確認だけします。

イベントとデバイスが揃ったら、イベントのc8y_Positionとデバイスのc8y_Geofenceからデータを抽出します。これらのオブジェクトは、 paramsにあるdictionary<any, any>にマッピングされます。paramsanyのタイプの値を保持するため、dictionary<any, any>にキャストする必要があります。

if(dev.params.hasKey("c8y_Geofence") and dev.supportedOperations.indexOf("c8y_Geofence") >= 0) {
	dictionary<any, any> evtPos := <dictionary<any, any> > e.params["c8y_Position"];
	float eventLat := <float> evtPos["lat"];
	float eventLng := <float> evtPos["lng"];

	dictionary<any,any> devGeofence := <dictionary<any,any> > dev.params["c8y_Geofence"];
	float centerLat := <float> devGeofence["lat"];
	float centerLng := <float> devGeofence["lng"];
	float maxDistance := <float> devGeofence["radius"];
}

ステップ 4: トリガーの作成

前述のように、現在のデバイスの位置とジオフェンスの中心からの距離が、設定されたジオフェンスの半径よりも大きい場合、デバイスはフェンスの外側にあります。 アラームをトリガーするには、イベント間でデバイスがジオフェンスに出入りしたかどうかを確認できるよう、2つのイベントが必要になります。

最初のステップでは、前述の関数を使用して距離を計算します:

float d := distance(centerLat, centerLng, eventLat, eventLng);

次に、以下を使用してこれをイベントとして再ルーティングします:

event LocationEventWithDistance {
	string source;
	float distance;
	Event e;
	float maxDistance;
}

...

route LocationEventWithDistance(e.source, d, e, maxDistance);

ソースをイベントに配置して、リスナーで簡単に一致できるようにします。

次に、LocationEventWithDistanceというイベントによってトリガーされるリスナーを設定し、同じソースに対して次のLocationEventWithDistanceをリッスンします:

on all LocationEventWithDistance() as firstPos {
	on LocationEventWithDistance(source = firstPos.source) as secondPos {
		// now have two events with distances
	}
}

このLocationEventWithDistanceというペアのイベントは、アラームを作成する必要があるかどうかを確認するためのすべてのデータを保持するようになりました。secondPosイベントをフィルタリングして、最初と同じソースに対応していることに注意してください。イベントを受信したすべてのデバイスに対して有効なリスナーが存在します。

ステップ 5: アラームの作成

アラームを作成するには、イベントの距離が半径よりも小さいものと、イベントの距離が半径よりも大きい2つのイベントが必要になります。これは、デバイスがジオフェンスを離れたことを意味します。

if firstPos.distance <= firstPos.maxDistance and
	secondPos.distance > secondPos.maxDistance {
	send Alarm("", "c8y_GeofenceAlarm", firstPos.source, currentTime,
					"Device moved out of circular geofence", "ACTIVE",
					"MAJOR", 1, new dictionary<string,any>) to Alarm.SEND_CHANNEL;
}

ステップ 6: アラームのクリア

アラームをクリアするには、最後の行にあるようにアラームの状態を変更し、さらに現在有効なアラームからアラームのIDを取得します。この時点で既存のアラームがあるかどうかを気にする必要はありません。存在しない場合、リスナーはand not FindAlarmResponseAckをトリガーし、リスナーを終了します:

monitor.subscribe(FindAlarmResponse.SUBSCRIBE_CHANNEL);
...
if firstPos.distance > firstPos.maxDistance and
    secondPos.distance <= secondPos.maxDistance {
    integer reqId:= integer.getUnique();
    send FindAlarm(reqId, {"source": firstPos.source, 
        "status": "ACTIVE", "type": "c8y_GeofenceAlarm"}) to FindAlarm.SEND_CHANNEL;
    on FindAlarmResponse(reqId=reqId) as alarmResponse
       and not FindAlarmResponseAck(reqId=reqId) {
        send Alarm(alarmResponse.id, "c8y_GeofenceAlarm",
                    firstPos.source, currentTime, "Device moved back into circular geofence",
                    "CLEARED", alarmResponse.alarm.severity, 1, new dictionary<string, any>) to Alarm.SEND_CHANNEL;
    }
}

すべてを統合する

これで、すべてのパーツを1つのモジュールに結合できます。リスナーの順序は任意の順序に変更可能です。

using com.apama.cumulocity.ManagedObject;
using com.apama.cumulocity.Measurement;
using com.apama.cumulocity.Event;
using com.apama.cumulocity.Alarm;
using com.apama.cumulocity.FindManagedObject;
using com.apama.cumulocity.FindManagedObjectResponse;
using com.apama.cumulocity.FindManagedObjectResponseAck;
using com.apama.cumulocity.FindAlarm;
using com.apama.cumulocity.FindAlarmResponse;
using com.apama.cumulocity.FindAlarmResponseAck;

monitor MonitorDevicesForCircularGeofence {

	event LocationEventWithDistance {
		string source;
		float distance;
		Event e;
		float maxDistance;
	}

	action onload {
		monitor.subscribe(Measurement.SUBSCRIBE_CHANNEL);
		monitor.subscribe(FindManagedObjectResponse.SUBSCRIBE_CHANNEL);
		monitor.subscribe(FindAlarmResponse.SUBSCRIBE_CHANNEL);
		on all Event() as e {
			if e.params.hasKey("c8y_Position") {
				// we have an event
				integer reqId := integer.getUnique();
				send FindManagedObject(reqId, e.source, new dictionary<string,string>) to FindManagedObject.SEND_CHANNEL;
				on FindManagedObjectResponse(reqId = reqId) as resp
				and not FindManagedObjectResponseAck(reqId) {
				ManagedObject dev := resp.managedObject;

				if(dev.params.hasKey("c8y_Geofence") and dev.supportedOperations.indexOf("c8y_Geofence") >= 0) {

						dictionary<any, any> evtPos := <dictionary<any, any> > e.params["c8y_Position"];
						float eventLat := <float> evtPos["lat"];
						float eventLng := <float> evtPos["lng"];

						dictionary<any,any> devGeofence := <dictionary<any,any> > dev.params["c8y_Geofence"];
						float centerLat := <float> devGeofence["lat"];
						float centerLng := <float> devGeofence["lng"];
						float maxDistance := <float> devGeofence["radius"];

						float d := distance(centerLat, centerLng, eventLat, eventLng);

						route LocationEventWithDistance(e.source, d, e, maxDistance);
					}
				}
			}
		}

		on all LocationEventWithDistance() as firstPos {
			on LocationEventWithDistance(source = firstPos.source) as secondPos {
				// now have two events with distances
				if firstPos.distance <= firstPos.maxDistance and
					secondPos.distance > secondPos.maxDistance {
					send Alarm("", "c8y_GeofenceAlarm", firstPos.source, currentTime,
							"Device moved out of circular geofence", "ACTIVE",
							"MAJOR", 1, new dictionary<string,any>) to Alarm.SEND_CHANNEL;
				}

				if firstPos.distance > firstPos.maxDistance and
					secondPos.distance <= secondPos.maxDistance {
					integer reqId:= integer.getUnique();
					send FindAlarm(reqId, {"source": firstPos.source, 
						"status": "ACTIVE", "type": "c8y_GeofenceAlarm"}) to FindAlarm.SEND_CHANNEL;
					on FindAlarmResponse(reqId=reqId) as alarmResponse
					and not FindAlarmResponseAck(reqId=reqId) {
						send Alarm(alarmResponse.id, "c8y_GeofenceAlarm",
								firstPos.source, currentTime, "Device moved back into circular geofence",
								"CLEARED", alarmResponse.alarm.severity, 1, new dictionary<string, any>) to Alarm.SEND_CHANNEL;
					}
				}
			}
		}
	}

	action distance(float lat1, float lon1, float lat2, float lon2) returns float {
		float R := 6371000.0;
		float toRad := float.PI / 180.0;
		float lat1Rad := lat1 * toRad;
		float lat2Rad := lat2 * toRad;
		float deltaLatRad := (lat2-lat1) * toRad;
		float deltaLonRad := (lat2-lat1) * toRad;
		float a := (deltaLatRad/2.0).sin().pow(2.0) * lat1Rad.cos() * lat2Rad.cos() * (deltaLonRad/2.0).sin().pow(2.0);
		float c := 2.0 * a.sqrt().atan2((1.0-a).sqrt());
		return R * c;
	}
}