概述

所谓规则引擎,指的是if some condition match then trigger some thing的机制。condition是一系列的expression,比如设备状态变更为离线(属性),考勤有人通过闸机(事件);trigger一系列的action,比如存储到数据库、发出告警信息。乃至于触发其他设备的动作,比如温度过高则判断火灾则触发喷淋联动。

将rule抽象出来,让用户可以自由定义,就是设计规则。关联好rule和action,本质就是一种回调注册机制,当condition匹配时,代码会query用户已经定义的action,并依次触发(当然还有服务端写死的一些action)。

可以match的condition,以及可以trigger的action,都是我们实现定义好的模版,用户可以在rule/action里面填入自定义的参数。因此设计规则引擎就是设计一种模版语言,前端可以渲染这种语言让用户填充参数,后端可以解析这种语言实现对应的逻辑。因为这相当于设计一种语言。举个例子,如果写python的话,可以直接用python写condition,然后eval计算结果。对于Java而言,开源的drools本质上是一门脚本语言,让客户端去解析这个工作量太大,不太可能采用;而urule的开源版功能过于孱弱,只能拿来参考。不过如果使用工作流引擎,比如flowable,内部集成了DMN决策树,本质上就是一个规则引擎,可以考虑直接使用,不过我们这里简化了一下用了自己的实现。

condition和action的设计依赖于设备本身能提供的功能,因此需要将设备具体的型号和能支持的condition/action关联起来。举例来说,如果考勤机支持温度上报,那么就可以支持定义体温报警。不支持的话,用户就没法设置体温报警。

规则引擎核心数据

由于我们使用网关作为中间层通信,mqtt协议的通信都是异步的。因此我们把rule都定义为设备触发的event,event关联到具体的字段(即属性,attr)。attr的类型(如bool、枚举值、日期、时间、整数、浮点和字符串)决定了关联的前端控件(一般就是开关、inputbox)和操作符(大于小于等于,字符串包含、正则匹配等)。用户拼接attr和值,组成复杂表达式,即建立了rule. event我们做最小化拆解,和设备本身无关,即厂商的model要么支持某个event,要么不支持,不存在只支持一部分的可能。

能够触发的action,一种是设备支持的,另一种是我们服务支持的。和event一样,我们需要定义允许用户自由触发的action(以及参数),然后把这些action和设备型号关联起来。用户创建rule以后,选择服务/设备支持的action并填入必须的参数,自由组合即可。

这里不同于阿里云的设计,因为我们所有的设备都需要我们自己接入,所以清楚这个event和cmd的边界,而阿里云作为PAAS平台,他是允许所有设备接入的,所以不能预定义这些东西。

这里仍然使用门禁类设备举例说明。支持event包括:

  1. 考勤数据上报(EVENT_UPLOAD_ATTEND),attr包括访客身份(personId, personName),时间(timestamp,类型为time), 人脸图像(image,类型)和进出方向(direction);
  2. 体温数据上报(EVENT_UPLOAD_TEMP),attr包括访客身份(personId, personName),时间(timestamp,类型为time),体温(temperature, 浮点数);

这里很多时候1和2是同时上报的,但是我们仍然按着最小化原则拆分成两个事件,这样就可以区分支持体温测量的设备型号,和不支持的。类似的,环境检测仪的数据可能一次上报很多条,我们仍然分别拆分成不同的事件,以便自由组合condition。

而这里可以trigger的action包括:

  1. 系统服务。比如告警,可以预定义几种告警给用户使用。包括:站内信、离线推送、短信推送、微信推送和电话报警等,作为参数给用户组合。
  2. 门禁设备本身支持的action,比如遥控开门等;
  3. 其他物联设备支持的action,比如工地的广播等;

用户可以做以下组合:

对EVENT_UPLOAD_ATTEND,if timestamp >= 21:00 and direction=2 then trigger alarm(param=站内信),即21点之后有人从门禁离开则使用站内信告警。

对于EVENT_UPLOAD_TEMP,if temperature >= 37.3 then trigger broadcaster 125 action 1 template 5,如果有人体温大于37.0度,则使用工地中id为125的广播执行动作1(假设就是语言广播),广播内容为模版5(预定义好的体温告警)。显然这里模版5可能会用到event中用户的名字,所以还要把event传入作为action的参数。

存储设计

首先定义iot_rule_base,即event/action的预定义(该表的内容目前是直接写库的,不通过界面编辑),例如:

{
  	id: int,
    rule_code:str, //规则唯一描述符,如EVENT_UPLOAD_BODY_TEMP体温上报,或者ACTION_SYS_NOTIFY系统通知
    device_type: int, //设备类型,为0标示全局支持
    rule_type: int, //事件还是动作
    name: str, //事件名、简单描述
    params:{  //json schema
      {
        "type": "object",
        "properties": {
          "time": {
            "type": "string",
            "title": "发生时间"
          },
          "direction": {
            "type": "integer",
            "title": "方向",
            "enum": [
              1,
              2
            ],
            "enumDesc": "进;出"
          },
          "personId": {
            "type": "string",
            "title": "用户ID"
          }
        }
      }
    }
}

然后我们定义iot_device_model表,将设备类型和event/action关联起来:

{
    id: long
    type: int,
    name: str, //型号的描述,比如:人脸识别(无测温),人脸识别(含测温)
    events:[1,2,3], //支持的事件,json array
    actions:[1,2,3] //支持的动作,json array
}

当前版本的设备型号关联预定义事件和动作还未显示在界面上,V2要加上(在设备型号编辑界面)。这里标示设备型号支持的事件和动作。

下面定义规则,即iot_rule_trigger

{
    id: long,
    group_code: str, //定义规则的组织
    device_type: int, //设备类型
    device_model: int, //设备型号ID;如果为0标示该类型的所有型号
    device_ids: [], //触发规则的设备列表;如果是该型号的全部设备,传入[0]
    event_code: str, //触发的事件唯一标示
    condition: str, //规则表达式(供后端解析)
    dom: str, //前端对应dom描述(供前端渲染)
    memo: str, //规则备注
}

v2版本将actions加了一层抽象,即 执行库。执行=动作+参数,即预设的参数化的动作。执行会关联到规则。定义为iot_rule_exec:

{
	id: long,
  group_code: str, //定义执行的组织
  name: str, //执行的名称
  params: {
  	"actOn":[{
        code:"ACTION_SYS_NOTIFY", 
        delay: 0, //延迟执行时间,单位:秒
        pushType:1, 
        cycle: 30, //最小触发周期,单位:秒
        level:1, //0-普通消息,1-4:x级
        targets:[int] //用户id
    	},{
      	code: "ACTION_SPRAY_SWITCH",
        delay: 300,
        children:[{ //子事件,严格按顺序执行
        	code:"ACTION_SYS_NOTIFY",
          delay: 0,
          pushType:1,
          level: 0,
          targets:[]
        }]
      }],
    "actOff":[]
  }, //json,触发规则时动作
  rules: [], //关联的规则列表
}

params固定有参数 act_on 和 act_off 标示进入/离开规则时触发的动作列表;后面是一个树状结构:同一级别在数组内的数据,可以并行运行; children 指定的子事件,必须在父事件之后运行。

多个执行之间是并行关系,没有顺序。

action/event数据结构设计

使用json schema语法,并做以下扩展:

  1. enum可以使用 enumCode ,表明后台字典的 featCode;如果是简单枚举,使用标准语法(参考上面的direction),此时enumDesc里面是以 英文分号 分割的中文描述;此外如果有 parentCode 字段,标示对应字典项还要筛选父节点(参考字典相关的API);还是以 direction 为例,假设字典代码为 ATTEND_DIR ,那么定义形式如下:
{
  "type": "object",
  "properties": {
    "direction": {
      "type": "integer",
      "title": "方向",
      "enumCode": "ATTEND_DIR"
    }
  }
}

如果不用enumCode,直接把进出选项写出来,那就是:

{
  "type": "object",
  "properties": {
    "direction": {
      "type": "integer",
      "title": "方向",
      "enum": [	//这里直接给了可选值和对应的desc
        1,
        2
      ],
      "enumDesc": "进;出"
    }
  }
}
  1. 有一个固定字段 time 标示事件发生的时间点,可以针对其做一些日期、星期、以及时间的配置。比如配置仅工作日的8-18点之间触发规则;但是这里要使用一些和通用格式不同的特殊配置。这个版本需求 暂时不做;
  2. 如果要对某些特殊用户的考勤进行告警,可能需要 personId 配置规则时加上人员选择器。 暂时不做;
  3. 某些字段会有 unit 属性,表示单位的中文描述,比如分贝等,用于发送消息或者前端展示;
  4. event第一层有一个 eventType 属性,默认是0,表示可自动解除;1表示不可自动解除;像AI报警/安全帽报警这种,属于不可解除的报警,此时每次事件触发都会创建新的告警记录,必须用户手动解除(换句话说此时执行中设置的“同质消息频率”选项是无效的。)
    condition语法

condition这里使用mongo query表达式,类似q参数最开始的设计,上面例子的表达式就是:

{
    "key1":{"$lt": 8}, //key1小于8; $lt(<), $gt(>), $le(<=), $ge(>=), $ne(!=), $eq(=)
    "key2":{"$ge": "37.3"}, //key2大于等于37.3
    "$or":[{"key3":{"$regex": "\d{3}"}},{"key4": "3"}], //key3满足正则,或者key4=3,使用$like表示包含
    "key4":{"$in":[1, 2, 3]}, //key4=1或2或3
    "key6.0": 10, //数组第一个元素是10
    "key7.subKey1":{"$lt": 10}, //object的子元素小于10

}

前端为了方便自己解析,可以使用另外一套语法,即前面提到的dom字段,服务器不会解析该字段。

现有Action

告警消息

rule_code: ACTION_SYS_NOTIFY 
{
  delay: 0, //延迟时间,单位:秒
  pushType:1, //推送类型,位运算,1:站内信,2:离线推送,4:短信,8:电话,16:智能广播,32:微信
  broaderIds: [1], //用户选择关联的音柱id,注意政企端是没有音柱可选的,所以这个字段必然为空或者没有
  cycle:60, //最小触发周期,单位秒,在此周期内同一类型的告警最多发一条通知
  level:1, //0-普通消息,1-4:风险等级
  targets:[],//推送目标id,注意如果是通过政企端设置的规则,则推送到政企端;否则推送到项目端
  targetRoles: [], //推送角色id,如果用户选择全部,则roleId=-1
  muteTimes:[["19:00", "07:00"]],//勿扰时间段,注意如果左边的更大,表明这个时间段是跨天的
  muteType: 1, //静默推送类型,也是位运算
}

喷淋联动

rule_code: ACTION_SPRAY_SWITCH

{
  turnOn: bool, //打开还是关闭
  delay: 0, //延迟打开
  deviceIds: [], //选择喷淋设备
}

理论上用户可以设置的action需要根据项目目前拥有的设备型号决定:后端获取项目的所有设备,然后将所有action取去重并集,返回给前端。

当前版本只需支持告警通知。

参考文献

  1. 从0到1:构建强大且易用的规则引擎