一、前情提要
-
现有物联网系统已经初步接入了一些智能设备并对相应的设备进行数据收集和控制。下一步是要实现设备的联动功能。
二、要做什么?
我们要实现的功能是,场景自动化功能。
- 当温度达到30℃时且湿度小于45%时 ==> 打开空调,并将空调调整为制冷模式,温度调节为20摄氏度
- 当济南的pm2.5浓度大于23时,==> 打开关闭插座,并发送钉钉消息提醒
-
当济南的天气状态为“雨天”时或室内光照强度小于250LUX时 ==> 打开空调除湿
这些任务,均可统一设置执行的时间段,并设置周几的执行状态。
三、怎么做?
1、难点
- 如何定义任务的数据结构,保证能够实现上面的功能。
- 如何通过代码动态去判断各种数据值的对比是否能够满足条件。
- 如何动态的去执行各个动作,且每个动作所需的参数不同。
- 如何优雅的实现,必然不能每个条件都写一大串 if - else
- 如何触发每个任务的执行,定义统一的触发入口
- 如何保证服务的稳定及可拓展,橙子便利现有140+门店,假设每家门店都有5个常规任务在执行,进行的任务数就要再700+。
2、表结构
CREATE TABLE `scene_automation` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增ID',
`scene_name` varchar(200) DEFAULT NULL COMMENT '场景名称',
`store_code` varchar(10) DEFAULT NULL COMMENT '门店编号',
`match_type` varchar(10) DEFAULT NULL COMMENT '条件匹配模式(ANY 任意满足 ALL 全部满足)',
`background` varchar(200) DEFAULT NULL COMMENT '背景',
`scene_state` varchar(10) DEFAULT NULL COMMENT '场景状态(ON 开启 OFF 关闭)',
`conditions` text COMMENT '条件',
`actions` text COMMENT '条件',
`preconditions` text COMMENT '前置条件',
`create_no` varchar(50) DEFAULT NULL COMMENT '创建人',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_no` varchar(50) DEFAULT NULL COMMENT '更新人',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='场景自动化表';
3、数据结构
{
"sceneName": "场景自动化测试",
"storeCode": "0009",
"matchType": "ALL",
"background": "#ffffff",
"sceneState": "ON",
"conditionMessages": [
{
"entityId": "6cb6a428adede6b580jqqb",
"entityType": "DEVICE",
"orderNum": 1,
"display": {
"code": "temperature",
"operator": "MORE",
"value": 25
}
},
{
"entityId": "6cb6a428adede6b580jqqb",
"entityType": "DEVICE",
"orderNum": 2,
"display": {
"code": "humidity",
"operator": "MORE",
"value": 30
}
}
],
"preconditionMessages": [
{
"display": {
"start": "00:00",
"end": "23:59",
"loops": "1111110"
},
"type": "TIME_CHECK"
}
],
"actionMessages": [
{
"executor": "socket",
"entityId": "6c30fb6e4c962c96b2asgo",
"property": {
"switchSocket": true
}
},
{
"executor": "sendDing",
"property": {
"message": "钉钉消息发送测试"
}
}
]
}
4、引入jexl逻辑引擎
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-jexl</artifactId>
<version>2.1.1</version>
</dependency>
Java表达式语言--Java Expression Language(JEXL),这是Apache开源的一个jar包,旨在促进在用Java编写的应用程序和框架中,实现动态和脚本功能,JEXL基于JSTL表达式语言的一些扩展实现了表达式语言,支持shell脚本或ECMAScript(js)中的大多数构造.
5、条件-核心逻辑封装(关系运算符)
/**
* 关系运算符
*
* @param conditionOperationMessage
* @return boolean
*/
private static boolean relationalCal(ConditionOperationMessage conditionOperationMessage) {
JexlEngine engine = new JexlEngine();
JexlContext context = new MapContext();
// 构建表达式
String command = " if ( a " +
conditionOperationMessage.getOperation() +
conditionOperationMessage.getValue() +
" ) { return true } else { return false } ";
// 设置变量值
context.set("a", conditionOperationMessage.getParam());
// 执行计算结果
boolean result = (boolean) engine.createExpression(command).evaluate(context);
log.info("关系运算结果 [ {} {} {} ] --> [{}] ", conditionOperationMessage.getParam(), conditionOperationMessage.getOperation(), conditionOperationMessage.getValue(), result);
return result;
}
6、条件-逻辑运算表达式
/**
* 逻辑或运算核心方法
*
* @param conditionOperationMessageList
* @return boolean
*/
private static boolean logicOrCoreCal(List<ConditionOperationMessage> conditionOperationMessageList) {
log.info("========开始进入逻辑或运算=====");
for (ConditionOperationMessage conditionOperationMessage : conditionOperationMessageList) {
if (relationalCal(conditionOperationMessage)) {
log.info("========满足逻辑或运算=====");
return true;
}
}
log.info("========不满足逻辑或运算=====");
return false;
}
/**
* 逻辑或运算核心方法
*
* @param conditionOperationMessageList
* @return boolean
*/
private static boolean logicAndCoreCal(List<ConditionOperationMessage> conditionOperationMessageList) {
log.info("========开始进入逻辑且运算=====");
for (ConditionOperationMessage conditionOperationMessage : conditionOperationMessageList) {
if (!relationalCal(conditionOperationMessage)) {
log.info("========不满足逻辑且运算=====");
return false;
}
}
log.info("========满足逻辑且运算=====");
// 校验是否全部为true
return true;
}
7、动作-动态方法执行
/**
* 执行动作
*
* @param actionMessageList
*/
public static void action(List<ActionMessage> actionMessageList) {
JexlEngine engine = new JexlEngine();
JexlContext context;
if (!CollectionUtils.isEmpty(actionMessageList)) {
for (ActionMessage actionMessage : actionMessageList) {
context = new MapContext();
String command = "ActionTool." + actionMessage.getExecutor() + "(property)";
context.set("ActionTool", ActionTool.class);
context.set("property", actionMessage.getProperty());
boolean result = (boolean) engine.createExpression(command).evaluate(context);
log.info("方法[{}]执行结果[{}]", actionMessage.getExecutor(), result);
}
}
}
/**
* 插座控制
*
* @param property
*/
public static boolean socket(ActionPropertyMessage property) {
log.info("插座控制[{}]", property.getSwitchSocket());
autoService.testService();
return true;
}
/**
* 插座控制
*
* @param property
*/
public static boolean sendDing(ActionPropertyMessage property) {
log.info("发送钉钉消息[{}]", property.getMessage());
return true;
}
/**
* 延迟
*
* @param property
*/
public static boolean delay(ActionPropertyMessage property) {
log.info("延时控制 时[{}] 分[{}] 秒[{}] ", property.getDelayHour(), property.getDelayMinutes(), property.getDelaySeconds());
return true;
}
/**
* 空调控制
*
* @param property
*/
public static boolean airCondition(ActionPropertyMessage property) {
log.info("空调控制 类型[{}] 值[{}]", property.getAirConditionType().getStr(), property.getAirConditionValue());
return true;
}
调用入口
/**
* 调用入口
*
* @param code 触发条件
* @return SimpleMessage
*/
@Override
public SimpleMessage imitate(String code) {
// 获取正在开启的活动
List<SceneAutomation> sceneAutomationList = sceneAutomationDao.getSuitableScene(code);
// 校验是否存在该场景
if (CollectionUtils.isEmpty(sceneAutomationList)) {
return new SimpleMessage(ErrorCodeEnum.NO, "查询不到该场景");
}
// 遍历场景
for (SceneAutomation sceneAutomation : sceneAutomationList) {
// 判断前置条件
List<PreconditionMessage> preconditionMessageList = JSON.parseArray(sceneAutomation.getPreconditions(), PreconditionMessage.class);
if (!PreconditionTool.judge(preconditionMessageList)) {
log.info("任务[{}] 不满足前置条件", sceneAutomation.getSceneName());
continue;
}
// 解析条件
List<ConditionMessage> conditionMessages = JSON.parseArray(sceneAutomation.getConditions(), ConditionMessage.class);
// 条件执行封装列表
List<ConditionOperationMessage> conditionOperationMessageList = new ArrayList<>();
// 获取条件值
conditionMessages.forEach(conditionMessage -> {
// 设备类型
if (ConditionEntityTypeEnum.DEVICE.equals(conditionMessage.getEntityType())) {
// 条件值
Object param = sceneAutomationDao.getDeviceParam(conditionMessage.getDisplay().getCode(), conditionMessage.getEntityId());
log.info("设备码[{}] 类型[{}] 当前值[{}] ", conditionMessage.getEntityId(), conditionMessage.getDisplay().getCode(), param);
conditionOperationMessageList.add(ConditionOperationMessage.builder()
.param(param)
.operation(conditionMessage.getDisplay().getOperator().getStr())
.value(conditionMessage.getDisplay().getValue())
.build());
} else {
// TODO 待定
}
});
// 判断条件是否执行完成
if (CollectionUtils.isEmpty(conditionOperationMessageList)
|| !ConditionTool.judge(conditionOperationMessageList, sceneAutomation.getMatchType())) {
log.info("任务[{}] 不满足条件", sceneAutomation.getSceneName());
continue;
}
// 执行动作
List<ActionMessage> actionMessages = JSON.parseArray(sceneAutomation.getActions(), ActionMessage.class);
// 执行
ActionTool.action(actionMessages);
}
return new SimpleMessage(ErrorCodeEnum.OK);
}
三、还有什么要补充的?
1、mysql 查询 json格式数据
/**
* 获取符合条件的场景
*
* @param code 条件值
* @return List<SceneAutomation>
*/
@Select("SELECT " +
" id, " +
" scene_name, " +
" store_code, " +
" match_type, " +
" background, " +
" scene_state, " +
" conditions, " +
" preconditions, " +
" actions " +
"FROM " +
" `scene_automation` " +
"WHERE " +
" scene_state = 'ON' " +
" AND JSON_CONTAINS( conditions, JSON_OBJECT( 'display', JSON_OBJECT( 'code', #{code} ) ) )")
List<SceneAutomation> getSuitableScene(@Param("code") String code);
/**
* 获取参数值
*
* @param key key
* @param deviceUid 设备码
* @return Object
*/
@Select(" SELECT " +
"device_detail ->> '$.${key}' as 'param' " +
"from store_devices WHERE " +
"device_uid = #{deviceUid} ")
Object getDeviceParam(@Param("key") String key, @Param("deviceUid") String deviceUid);
2、static 穿透 service 执行
/**
* static穿透service执行方法
*/
final
AutoService innerAutoService;
static AutoService autoService;
public ActionTool(AutoService innerAutoService) {
this.innerAutoService = innerAutoService;
}
/**
* static穿透service初始化
*/
@PostConstruct
public void init() {
autoService = innerAutoService;
}
四、这玩意儿的意义是什么?
1、我们在遇到特殊问题的时候,要打破固有的编程思想,根据自己的业务需求找到最优的解决方案。
2、不要妥协,要让自己的程序变得优雅。