规则引擎设计
概述
所谓规则引擎,指的是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包括:
- 考勤数据上报(EVENT_UPLOAD_ATTEND),attr包括访客身份(personId, personName),时间(timestamp,类型为time), 人脸图像(image,类型)和进出方向(direction);
- 体温数据上报(EVENT_UPLOAD_TEMP),attr包括访客身份(personId, personName),时间(timestamp,类型为time),体温(temperature, 浮点数);
这里很多时候1和2是同时上报的,但是我们仍然按着最小化原则拆分成两个事件,这样就可以区分支持体温测量的设备型号,和不支持的。类似的,环境检测仪的数据可能一次上报很多条,我们仍然分别拆分成不同的事件,以便自由组合condition。
而这里可以trigger的action包括:
- 系统服务。比如告警,可以预定义几种告警给用户使用。包括:站内信、离线推送、短信推送、微信推送和电话报警等,作为参数给用户组合。
- 门禁设备本身支持的action,比如遥控开门等;
- 其他物联设备支持的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语法,并做以下扩展:
- 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": "进;出"
}
}
}
- 有一个固定字段 time 标示事件发生的时间点,可以针对其做一些日期、星期、以及时间的配置。比如配置仅工作日的8-18点之间触发规则;但是这里要使用一些和通用格式不同的特殊配置。这个版本需求 暂时不做;
- 如果要对某些特殊用户的考勤进行告警,可能需要 personId 配置规则时加上人员选择器。 暂时不做;
- 某些字段会有 unit 属性,表示单位的中文描述,比如分贝等,用于发送消息或者前端展示;
- 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取去重并集,返回给前端。
当前版本只需支持告警通知。