开发者中心

研究:圆形地理围栏报警

概述

下面所示的模块将给出一个深入的例子演示如何创建更复杂的规则。它将用到其他指南部分已经介绍的多个功能。 如果刚刚接触QuarkIoE事件语言,请参看 例子

前提条件

目标

我们希望我们跟踪的设备,连续发送定位事件,如果它们移出地理围栏外将自动生成报警。 这个地理围栏是一个圆,可以为每个设备单独配置。设备移动到地理围栏外将生成报警。 当设备在地理围栏外面移动时,不会生成新的报警,因为第一个报警依然有效,为避免重复会过滤新生成的报警 一旦当设备移动回到地理围栏内报警可以清除。

QuarkIoE数据模型

Location event structure (the part we need):


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

在设备里我们如何保存地理围栏设置(半径单位是米):


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

另外我们每个设备可以不完全删除配置的情况下可以启用/禁用地理围栏报警。 我们通过添加/删除 "c8y_Geofence" 到设备的 c8y_SupportedOperations实现:


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

如果当前位置和中心之间的距离大于配置的地理围栏半径则设备在该地理围栏外面。 我们需要的是一个函数来计算地理坐标之间的差异:


  create expression distance(lat1, lon1, lat2, lon2) [
    var R = 6371000;
    var toRad = function(arg) {
    return arg * Math.PI / 180;
  };
  var lat1Rad = toRad(lat1);
  var lat2Rad = toRad(lat2);
  var deltaLatRad = toRad(lat2-lat1);
  var deltaLonRad = toRad(lon2-lon1);

  var a = Math.sin(deltaLatRad/2) * Math.sin(deltaLatRad/2) +
  Math.cos(lat1Rad) * Math.cos(lat2Rad) * Math.sin(deltaLonRad/2) * Math.sin(deltaLonRad/2);

  var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));

  var d = R * c;
  d;
  ];
             

上面的函数返回以米为单位的距离。

步骤 1: 过滤输入

该模块的主要输入是事件。为了尽早丢弃不匹配的事件,我们将在语句中创建一个过滤器,只匹配包含位置的事件。 这些将被放入一个新的流。


  create schema LocationEvent(
    event Event
    );

    @Name('Location_event_filter')
    insert into LocationEvent
    select
    e.event as event
    from EventCreated e
    where getObject(e, "c8y_Position") is not null;
                

为哦么不需要EventCreated的附加信息,只是把有效载荷 (事件) 发送到下一个流。

步骤 2: 收集需要的数据

下一步我们需要为计算配置地理围栏并获取它。与事件一起,转发到下一个流。


  create schema LocationEventAndDevice (
    event Event,
    device ManagedObject
    );

    @Name('fetch_event_device')
    insert into LocationEventAndDevice
    select
    e.event as event,
    findManagedObjectById(event.source.value) as device
    from LocationEvent e;
              

Step 3: 检查设备是否支持 c8y_Geofence

现在检查设备是否设置了地理围栏,是否是激活状态 (c8y_SupportedOperations中包含 "c8y_Geofence")。 从设备中用anyOf()函数提取 c8y_SupportedOperations 数组。该函数遍历所有元素,如果元素表达式为true则返回true。 对于配置我们只检查设备是否包含片段 "c8y_Geofence"


  create schema LocationEventWithGeofenceConfig (
    event Event,
    eventLat Number,
    eventLng Number,
    centerLat Number,
    centerLng Number,
    maxDistance Number
    );

    @Name('parse_event_and_device_fragments')
    insert into LocationEventWithGeofenceConfig
    select
    c.event as event,
    getNumber(e.event, "c8y_Position.lat") as eventLat,
    getNumber(e.event, "c8y_Position.lng") as eventLng,
    getNumber(e.device, "c8y_Geofence.lat") as centerLat,
    getNumber(e.device, "c8y_Geofence.lng") as centerLng,
    getNumber(e.device, "c8y_Geofence.radius") as maxDistance
    from LocationEventAndDevice e
    where  
    getList(e.device, "c8y_SupportedOperations", new ArrayList()).anyOf(el => el = "c8y_Geofence") = true
    and getObject(e.device, "c8y_Geofence") is not null;
                

步骤 4: 创建触发器

如前所述,如果当前设备位置和地理围栏中心之间的距离大于设定的地理围栏半,径则设备在地理围栏外。 我们需要2个事件触发报警,我们检查在这两个事件之间,设备是否进入或离开地理围栏。

在第一步中,我们用前面提到的函数计算距离


  create schema LocationEventWithDistance (
    event Event,
    maxDistance Number,
    distance Number
    );

    @Name('calculate_current_distance')
    insert into LocationEventWithDistance
    select
    e.event as event,
    e.maxDistance as maxDistance,
    cast(distance(centerLat, centerLng, eventLat, eventLng), java.lang.Number) as distance
    from LocationEventWithGeofenceConfig e;
                  

现在我们创建了一个事件窗口,它包含最后两个事件


  create schema LocationEventWithDistancePair (
    firstPos LocationEventWithDistance,
    secondPos LocationEventWithDistance
    );

    @Name('last_two_positions')
    insert into LocationEventWithDistancePair
    select
    first(*) as firstPos,
    last(*) as secondPos
    from LocationEventWithDistance.win:length(2);
                  

现在流LocationEventWithDistancePair包含了全部决定是否创建报警的所需数据。

步骤 5: 创建报警

要创建报警,我们现在需要两个事件,其中第一个距离小于半径,第二个距离大于半径。 这就意味着设备刚离开地理围栏。


@Name('create_geofence_alarm')
    insert into CreateAlarm
    select
    pair.firstPos.event.source as source,
    "ACTIVE" as status,
    current_timestamp().toDate() as time,
    "c8y_GeofenceAlarm" as type,
    "MAJOR" as severity,
    "Device moved out of circular geofence" as text
    from LocationEventWithDistancePair pair
    where pair.firstPos.distance.doubleValue() <= pair.firstPos.maxDistance.doubleValue()
    and pair.secondPos.distance.doubleValue() > pair.secondPos.maxDistance.doubleValue();
                   

步骤 6: 清除

要清除报警,我们只需要在最下面切换条件,另外获取当前活动的报警,以得到ID。 我们不需要关心当前是否存在报警。如果没有报警此语句将不会成功执行,因为该函数将返回空值。


  @Name('clear_geofence_alarm')
    insert into UpdateAlarm
    select
    findFirstAlarmBySourceAndStatusAndType(pair.firstPos.event.source.value, "ACTIVE", "c8y_GeofenceAlarm").getId().getValue() as id,
    "Device moved back into circular geofence" as text,
    "CLEARED" as status
    from LocationEventWithDistancePair as pair
    where pair.firstPos.distance.doubleValue() > pair.firstPos.maxDistance.doubleValue()
    and pair.secondPos.distance.doubleValue() <= pair.secondPos.maxDistance.doubleValue();
                      

步骤 7: 创建设备上下文

我们的规则已经开始运行了,但还遗留一个问题。到目前为止,我们没有注意到是什么设备发送的位置事件。 如果设备A在地理围栏内发送位置事件,而下一个事件是设备B从地理围栏外发送,按规则将产生一个报警。 设备A会生成一个报警,因为我们采用的第一个到达事件的来源作为报警来源。 我们需要配置一个事件窗口,该窗口只应该包含同一个设备的最新的两个事件。 如果从另一个设备接受一个事件,应该创建一个新的窗口,以便最后对每个有一个单独的窗口。

这可以通过上下文实现。我们只在创建窗口的时候需要上下文。 上下文的分区应该是设备ID,以便引擎自动为每个设备创建一个单独的上下文分区。


    create context GeofenceDeviceContext
     partition by event.source.value from LocationEventWithDistance;
                         

现在我们可以将上下文添加到创建窗口的语句中。上下文仅对输入在上下文中配置的语句有效。 否则,引擎不知道要创建上下文分区的值。


   @Name('last_two_positions')
    context GeofenceDeviceContext
    insert into LocationEventWithDistancePair
    select
    first(*) as firstPos,
    last(*) as secondPos
    from LocationEventWithDistance.win:length(2);
                        

合并以上所有工作

我们现在可以将所有的部分组合成一个模块。顺序无所谓。 唯一的例外是,如果您使用自定义模型(如模式, 函数, 上下文, 变量, ...),需要在使用之前声明。


  create expression distance(lat1, lon1, lat2, lon2) [
    var R = 6371000;
    var toRad = function(arg) {
    return arg * Math.PI / 180;
  };
  var lat1Rad = toRad(lat1);
  var lat2Rad = toRad(lat2);
  var deltaLatRad = toRad(lat2-lat1);
  var deltaLonRad = toRad(lon2-lon1);

  var a = Math.sin(deltaLatRad/2) * Math.sin(deltaLatRad/2) +
  Math.cos(lat1Rad) * Math.cos(lat2Rad) * Math.sin(deltaLonRad/2) * Math.sin(deltaLonRad/2);

  var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));

  var d = R * c;
  d;
  ];

  create schema LocationEvent(
  event Event
  );

  create schema LocationEventAndDevice (
  event Event,
  device ManagedObject
  );

  create schema LocationEventWithGeofenceConfig (
  event Event,
  eventLat Number,
  eventLng Number,
  centerLat Number,
  centerLng Number,
  maxDistance Number
  );

  create schema LocationEventWithDistance (
  event Event,
  maxDistance Number,
  distance Number
  );

  create schema LocationEventWithDistancePair (
  firstPos LocationEventWithDistance,
  secondPos LocationEventWithDistance
  );

  create context GeofenceDeviceContext
  partition by event.source.value from LocationEventWithDistance;

  @Name('Location_event_filter')
  insert into LocationEvent
  select
  e.event as event
  from EventCreated e
  where getObject(e, "c8y_Position") is not null;

  @Name('fetch_event_device')
  insert into LocationEventAndDevice
  select
  e.event as event,
  findManagedObjectById(event.source.value) as device
  from LocationEvent e;

  @Name('parse_event_and_device_fragments')
  insert into LocationEventWithGeofenceConfig
  select
  c.event as event,
  getNumber(e.event, "c8y_Position.lat") as eventLat,
  getNumber(e.event, "c8y_Position.lng") as eventLng,
  getNumber(e.device, "c8y_Geofence.lat") as centerLat,
  getNumber(e.device, "c8y_Geofence.lng") as centerLng,
  getNumber(e.device, "c8y_Geofence.radius") as maxDistance
  from LocationEventAndDevice e
  where  
  getList(e.device, "c8y_SupportedOperations", new ArrayList()).anyOf(el => el = "c8y_Geofence") = true
  and getObject(e.device, "c8y_Geofence") is not null;

  @Name('calculate_current_distance')
  insert into LocationEventWithDistance
  select
  e.event as event,
  e.maxDistance as maxDistance,
  cast(distance(centerLat, centerLng, eventLat, eventLng), java.lang.Number) as distance
  from LocationEventWithGeofenceConfig e;

  @Name('last_two_positions')
  context GeofenceDeviceContext
  insert into LocationEventWithDistancePair
  select
  first(*) as firstPos,
  last(*) as secondPos
  from LocationEventWithDistance.win:length(2);

  @Name('create_geofence_alarm')
  insert into CreateAlarm
  select
  pair.firstPos.event.source as source,
  "ACTIVE" as status,
  current_timestamp().toDate() as time,
  "c8y_GeofenceAlarm" as type,
  "MAJOR" as severity,
  "Device moved out of circular geofence" as text
  from LocationEventWithDistancePair pair
  where pair.firstPos.distance.doubleValue() <= pair.firstPos.maxDistance.doubleValue()
  and pair.secondPos.distance.doubleValue() > pair.secondPos.maxDistance.doubleValue();

  @Name('clear_geofence_alarm')
  insert into UpdateAlarm
  select
  findFirstAlarmBySourceAndStatusAndType(pair.firstPos.event.source.value, "ACTIVE", "c8y_GeofenceAlarm").getId().getValue() as id,
  "Device moved back into circular geofence" as text,
  "CLEARED" as status
  from LocationEventWithDistancePair as pair
  where pair.firstPos.distance.doubleValue() > pair.firstPos.maxDistance.doubleValue()
  and pair.secondPos.distance.doubleValue() <= pair.secondPos.maxDistance.doubleValue();